You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ja...@apache.org on 2024/01/05 14:47:55 UTC

(solr) branch branch_9x updated: SOLR-15960 Unified use of system properties and environment variables (#1935)

This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 1da0e36df4d SOLR-15960 Unified use of system properties and environment variables (#1935)
1da0e36df4d is described below

commit 1da0e36df4d63f042daf32c45b1513784c210ecc
Author: Jan Høydahl <ja...@apache.org>
AuthorDate: Fri Jan 5 15:44:17 2024 +0100

    SOLR-15960 Unified use of system properties and environment variables (#1935)
    
    (cherry picked from commit 10bbabb473b8f898fbf36f364f2a3041c26988dc)
---
 solr/CHANGES.txt                                   |   2 +
 solr/bin/solr                                      |  40 +--
 solr/bin/solr.cmd                                  |   8 -
 solr/bin/solr.in.cmd                               |   5 +-
 solr/bin/solr.in.sh                                |   5 +-
 .../core/src/java/org/apache/solr/cli/SolrCLI.java |  16 +-
 .../org/apache/solr/core/TracerConfigurator.java   |   5 +-
 .../src/java/org/apache/solr/pkg/PackageAPI.java   |   4 +-
 .../src/java/org/apache/solr/util/EnvUtils.java    | 277 +++++++++++++++++++++
 .../src/java/org/apache/solr/util/ModuleUtils.java |  13 +-
 .../org/apache/solr/util/StartupLoggingUtils.java  |   2 +-
 .../circuitbreaker/CircuitBreakerRegistry.java     |   5 +-
 .../src/resources/EnvToSyspropMappings.properties  |  97 ++++++++
 .../test/org/apache/solr/util/EnvUtilsTest.java    | 119 +++++++++
 .../pages/property-substitution.adoc               |   6 +-
 .../pages/jwt-authentication-plugin.adoc           |   2 +-
 .../pages/solr-control-script-reference.adoc       |   6 +
 .../pages/major-changes-in-solr-9.adoc             |   3 +
 .../src/java/org/apache/solr/SolrTestCase.java     |   3 +-
 19 files changed, 540 insertions(+), 78 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 9879ab1c322..18b8ee99b57 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -59,6 +59,8 @@ Improvements
   using the `withDefaultCollection` method.  This is preferable to including the collection
   in the base URL accepted by certain client implementations. (Jason Gerlowski)
 
+* SOLR-15960: Unified use of system properties and environment variables (janhoy)
+
 Optimizations
 ---------------------
 * SOLR-17084: LBSolrClient (used by CloudSolrClient) now returns the count of core tracked as not live AKA zombies
diff --git a/solr/bin/solr b/solr/bin/solr
index 5737e5fd955..278c6177924 100755
--- a/solr/bin/solr
+++ b/solr/bin/solr
@@ -112,6 +112,13 @@ elif [ -r "$SOLR_INCLUDE" ]; then
   . "$SOLR_INCLUDE"
 fi
 
+# Export variables we want to make visible to Solr sub-process
+for var in $(compgen -e); do
+  if [[ "$var" =~ ^(SOLR_.*|DEFAULT_CONFDIR|ZK_.*|GCS_BUCKET|GCS_.*|S3_.*|OTEL_.*|AWS_.*)$ ]]; then
+    export "${var?}"
+  fi
+done
+
 # if pid dir is unset, default to $solr_tip/bin
 : "${SOLR_PID_DIR:=$SOLR_TIP/bin}"
 
@@ -1833,12 +1840,10 @@ if [ $# -gt 0 ]; then
         ;;
         -v)
             SOLR_LOG_LEVEL=DEBUG
-            PASS_TO_RUN_EXAMPLE+=("-Dsolr.log.level=$SOLR_LOG_LEVEL")
             shift
         ;;
         -q)
             SOLR_LOG_LEVEL=WARN
-            PASS_TO_RUN_EXAMPLE+=("-Dsolr.log.level=$SOLR_LOG_LEVEL")
             shift
         ;;
         -all)
@@ -1872,15 +1877,6 @@ if [ $# -gt 0 ]; then
   done
 fi
 
-if [[ -n ${SOLR_LOG_LEVEL:-} ]] ; then
-  SOLR_LOG_LEVEL_OPT="-Dsolr.log.level=$SOLR_LOG_LEVEL"
-fi
-
-# Solr modules option
-if [[ -n "${SOLR_MODULES:-}" ]] ; then
-  SCRIPT_SOLR_OPTS+=("-Dsolr.modules=$SOLR_MODULES")
-fi
-
 # Default placement plugin
 if [[ -n "${SOLR_PLACEMENTPLUGIN_DEFAULT:-}" ]] ; then
   SCRIPT_SOLR_OPTS+=("-Dsolr.placementplugin.default=$SOLR_PLACEMENTPLUGIN_DEFAULT")
@@ -1894,26 +1890,6 @@ if [ "${SOLR_ENABLE_STREAM_BODY:-false}" == "true" ]; then
   SCRIPT_SOLR_OPTS+=("-Dsolr.enableStreamBody=true")
 fi
 
-# Parse global circuit breaker env vars and convert to dot separated, lowercase properties
-if [ -n "${SOLR_CIRCUITBREAKER_UPDATE_CPU:-}" ]; then
-  SOLR_OPTS+=("-Dsolr.circuitbreaker.update.cpu=$SOLR_CIRCUITBREAKER_UPDATE_CPU")
-fi
-if [ -n "${SOLR_CIRCUITBREAKER_UPDATE_MEM:-}" ]; then
-  SOLR_OPTS+=("-Dsolr.circuitbreaker.update.mem=$SOLR_CIRCUITBREAKER_UPDATE_MEM")
-fi
-if [ -n "${SOLR_CIRCUITBREAKER_UPDATE_LOADAVG:-}" ]; then
-  SOLR_OPTS+=("-Dsolr.circuitbreaker.update.loadavg=$SOLR_CIRCUITBREAKER_UPDATE_LOADAVG")
-fi
-if [ -n "${SOLR_CIRCUITBREAKER_QUERY_CPU:-}" ]; then
-  SOLR_OPTS+=("-Dsolr.circuitbreaker.query.cpu=$SOLR_CIRCUITBREAKER_QUERY_CPU")
-fi
-if [ -n "${SOLR_CIRCUITBREAKER_QUERY_MEM:-}" ]; then
-  SOLR_OPTS+=("-Dsolr.circuitbreaker.query.mem=$SOLR_CIRCUITBREAKER_QUERY_MEM")
-fi
-if [ -n "${SOLR_CIRCUITBREAKER_QUERY_LOADAVG:-}" ]; then
-  SOLR_OPTS+=("-Dsolr.circuitbreaker.query.loadavg=$SOLR_CIRCUITBREAKER_QUERY_LOADAVG")
-fi
-
 : ${SOLR_SERVER_DIR:=$DEFAULT_SERVER_DIR}
 
 if [ ! -e "$SOLR_SERVER_DIR" ]; then
@@ -2391,7 +2367,7 @@ function start_solr() {
   fi
 
   SOLR_START_OPTS=('-server' "${JAVA_MEM_OPTS[@]}" "${GC_TUNE_ARR[@]}" "${GC_LOG_OPTS[@]}" "${IP_ACL_OPTS[@]}" \
-    "${REMOTE_JMX_OPTS[@]}" "${CLOUD_MODE_OPTS[@]}" ${SOLR_LOG_LEVEL_OPT:-} -Dsolr.log.dir="$SOLR_LOGS_DIR" \
+    "${REMOTE_JMX_OPTS[@]}" "${CLOUD_MODE_OPTS[@]}" -Dsolr.log.dir="$SOLR_LOGS_DIR" \
     "-Djetty.port=$SOLR_PORT" "-DSTOP.PORT=$stop_port" "-DSTOP.KEY=$STOP_KEY" \
     # '-OmitStackTraceInFastThrow' ensures stack traces in errors,
     # users who don't care about useful error msgs can override in SOLR_OPTS with +OmitStackTraceInFastThrow
diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd
index ed08f6d1d60..3856862137a 100755
--- a/solr/bin/solr.cmd
+++ b/solr/bin/solr.cmd
@@ -763,13 +763,11 @@ goto parse_args
 
 :set_debug
 set SOLR_LOG_LEVEL=DEBUG
-set "PASS_TO_RUN_EXAMPLE=!PASS_TO_RUN_EXAMPLE! -Dsolr.log.level=%SOLR_LOG_LEVEL%"
 SHIFT
 goto parse_args
 
 :set_warn
 set SOLR_LOG_LEVEL=WARN
-set "PASS_TO_RUN_EXAMPLE=!PASS_TO_RUN_EXAMPLE! -Dsolr.log.level=%SOLR_LOG_LEVEL%"
 SHIFT
 goto parse_args
 
@@ -1011,11 +1009,6 @@ IF NOT "%SOLR_HOST%"=="" (
 
 set SCRIPT_SOLR_OPTS=
 
-REM Solr modules option
-IF DEFINED SOLR_MODULES (
-  set "SCRIPT_SOLR_OPTS=%SCRIPT_SOLR_OPTS% -Dsolr.modules=%SOLR_MODULES%"
-)
-
 REM Default placement plugin
 IF DEFINED SOLR_PLACEMENTPLUGIN_DEFAULT (
   set "SCRIPT_SOLR_OPTS=%SCRIPT_SOLR_OPTS% -Dsolr.placementplugin.default=%SOLR_PLACEMENTPLUGIN_DEFAULT%"
@@ -1419,7 +1412,6 @@ IF "%SOLR_SSL_ENABLED%"=="true" (
   set "SSL_PORT_PROP=-Dsolr.jetty.https.port=%SOLR_PORT%"
   set "START_OPTS=%START_OPTS% %SOLR_SSL_OPTS% !SSL_PORT_PROP!"
 )
-IF NOT "%SOLR_LOG_LEVEL%"=="" set "START_OPTS=%START_OPTS% -Dsolr.log.level=%SOLR_LOG_LEVEL%"
 
 set SOLR_LOGS_DIR_QUOTED="%SOLR_LOGS_DIR%"
 set SOLR_DATA_HOME_QUOTED="%SOLR_DATA_HOME%"
diff --git a/solr/bin/solr.in.cmd b/solr/bin/solr.in.cmd
index f9892d33d66..77b1117208a 100755
--- a/solr/bin/solr.in.cmd
+++ b/solr/bin/solr.in.cmd
@@ -100,7 +100,10 @@ REM start command line as-is, in ADDITION to other options. If you specify the
 REM -a option on start script, those options will be appended as well. Examples:
 REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.autoSoftCommit.maxTime=3000
 REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.autoCommit.maxTime=60000
-REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.clustering.enabled=true
+
+REM Most properties have an environment variable equivalent.
+REM A naming convention is that SOLR_FOO_BAR maps to solr.foo.bar
+REM SOLR_CLUSTERING_ENABLED=true
 
 REM Path to a directory for Solr to store cores and their data. By default, Solr will use server\solr
 REM If solr.xml is not stored in ZooKeeper, this directory needs to contain solr.xml
diff --git a/solr/bin/solr.in.sh b/solr/bin/solr.in.sh
index 31cc131dfd3..37b37107dfb 100644
--- a/solr/bin/solr.in.sh
+++ b/solr/bin/solr.in.sh
@@ -105,7 +105,10 @@
 # -a option on start script, those options will be appended as well. Examples:
 #SOLR_OPTS="$SOLR_OPTS -Dsolr.autoSoftCommit.maxTime=3000"
 #SOLR_OPTS="$SOLR_OPTS -Dsolr.autoCommit.maxTime=60000"
-#SOLR_OPTS="$SOLR_OPTS -Dsolr.clustering.enabled=true"
+
+# Most properties have an environment variable equivalent.
+# A naming convention is that SOLR_FOO_BAR maps to solr.foo.bar
+#SOLR_CLUSTERING_ENABLED=true
 
 # Location where the bin/solr script will save PID files for running instances
 # If not set, the script will create PID files in $SOLR_TIP/bin
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
index cd619d49709..de3e37a4d58 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -61,6 +61,7 @@ import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.ContentStreamBase;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.util.EnvUtils;
 import org.apache.solr.util.StartupLoggingUtils;
 import org.apache.solr.util.configuration.SSLConfigurationsFactory;
 import org.slf4j.Logger;
@@ -189,18 +190,9 @@ public class SolrCLI implements CLIO {
   }
 
   public static String getDefaultSolrUrl() {
-    String scheme = System.getenv("SOLR_URL_SCHEME");
-    if (scheme == null) {
-      scheme = "http";
-    }
-    String host = System.getenv("SOLR_TOOL_HOST");
-    if (host == null) {
-      host = "localhost";
-    }
-    String port = System.getenv("SOLR_PORT");
-    if (port == null) {
-      port = "8983";
-    }
+    String scheme = EnvUtils.getEnv("SOLR_URL_SCHEME", "http");
+    String host = EnvUtils.getEnv("SOLR_TOOL_HOST", "localhost");
+    String port = EnvUtils.getEnv("SOLR_PORT", "8983");
     return String.format(Locale.ROOT, "%s://%s:%s", scheme.toLowerCase(Locale.ROOT), host, port);
   }
 
diff --git a/solr/core/src/java/org/apache/solr/core/TracerConfigurator.java b/solr/core/src/java/org/apache/solr/core/TracerConfigurator.java
index 30ce2071465..9e48baaa35e 100644
--- a/solr/core/src/java/org/apache/solr/core/TracerConfigurator.java
+++ b/solr/core/src/java/org/apache/solr/core/TracerConfigurator.java
@@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.util.EnvUtils;
 import org.apache.solr.util.plugin.NamedListInitializedPlugin;
 import org.apache.solr.util.tracing.SimplePropagator;
 import org.slf4j.Logger;
@@ -38,10 +39,10 @@ public abstract class TracerConfigurator implements NamedListInitializedPlugin {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   public static final boolean TRACE_ID_GEN_ENABLED =
-      Boolean.parseBoolean(System.getProperty("solr.alwaysOnTraceId", "true"));
+      Boolean.parseBoolean(EnvUtils.getProp("solr.alwaysOnTraceId", "true"));
 
   private static final String DEFAULT_CLASS_NAME =
-      System.getProperty(
+      EnvUtils.getProp(
           "solr.otelDefaultConfigurator", "org.apache.solr.opentelemetry.OtelTracerConfigurator");
 
   public abstract Tracer getTracer();
diff --git a/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java b/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java
index a1bc228cb72..37954cde5ee 100644
--- a/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java
+++ b/solr/core/src/java/org/apache/solr/pkg/PackageAPI.java
@@ -47,6 +47,7 @@ import org.apache.solr.core.CoreContainer;
 import org.apache.solr.filestore.FileStoreAPI;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.EnvUtils;
 import org.apache.solr.util.SolrJacksonAnnotationInspector;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.WatchedEvent;
@@ -57,8 +58,7 @@ import org.slf4j.LoggerFactory;
 
 /** This implements the public end points (/api/cluster/package) of package API. */
 public class PackageAPI {
-  public final boolean enablePackages =
-      Boolean.parseBoolean(System.getProperty("enable.packages", "false"));
+  public final boolean enablePackages = EnvUtils.getPropAsBool("enable.packages", false);
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   public static final String ERR_MSG =
diff --git a/solr/core/src/java/org/apache/solr/util/EnvUtils.java b/solr/core/src/java/org/apache/solr/util/EnvUtils.java
new file mode 100644
index 00000000000..721ce677791
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/util/EnvUtils.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.common.util.Utils;
+
+/**
+ * This class is a unified provider of environment variables and system properties. It exposes a
+ * mutable copy of the environment variables. It also converts 'SOLR_FOO' variables to system
+ * properties 'solr.foo' and provide various convenience accessors for them.
+ */
+public class EnvUtils {
+  private static final SortedMap<String, String> ENV = new TreeMap<>(System.getenv());
+  private static final Map<String, String> CUSTOM_MAPPINGS = new HashMap<>();
+  private static final Map<String, String> camelCaseToDotsMap = new HashMap<>();
+
+  static {
+    try {
+      Properties props = new Properties();
+      try (InputStream stream =
+          EnvUtils.class.getClassLoader().getResourceAsStream("EnvToSyspropMappings.properties")) {
+        props.load(new InputStreamReader(Objects.requireNonNull(stream), StandardCharsets.UTF_8));
+        for (String key : props.stringPropertyNames()) {
+          CUSTOM_MAPPINGS.put(key, props.getProperty(key));
+        }
+        init(false);
+      }
+    } catch (IOException e) {
+      throw new SolrException(
+          SolrException.ErrorCode.INVALID_STATE, "Failed loading env.var->properties mapping", e);
+    }
+  }
+
+  /**
+   * Get Solr's mutable copy of all environment variables.
+   *
+   * @return sorted map of environment variables
+   */
+  public static SortedMap<String, String> getEnvs() {
+    return ENV;
+  }
+
+  /** Get a single environment variable as string */
+  public static String getEnv(String key) {
+    return ENV.get(key);
+  }
+
+  /** Get a single environment variable as string, or default */
+  public static String getEnv(String key, String defaultValue) {
+    return ENV.getOrDefault(key, defaultValue);
+  }
+
+  /** Get an environment variable as long */
+  public static long getEnvAsLong(String key) {
+    return Long.parseLong(ENV.get(key));
+  }
+
+  /** Get an environment variable as long, or default value */
+  public static long getEnvAsLong(String key, long defaultValue) {
+    String value = ENV.get(key);
+    if (value == null) {
+      return defaultValue;
+    }
+    return Long.parseLong(value);
+  }
+
+  /** Get an env var as boolean */
+  public static boolean getEnvAsBool(String key) {
+    return StrUtils.parseBool(ENV.get(key));
+  }
+
+  /** Get an env var as boolean, or default value */
+  public static boolean getEnvAsBool(String key, boolean defaultValue) {
+    String value = ENV.get(key);
+    if (value == null) {
+      return defaultValue;
+    }
+    return StrUtils.parseBool(value);
+  }
+
+  /** Get comma separated strings from env as List */
+  public static List<String> getEnvAsList(String key) {
+    return getEnv(key) != null ? stringValueToList(getEnv(key)) : null;
+  }
+
+  /** Get comma separated strings from env as List */
+  public static List<String> getEnvAsList(String key, List<String> defaultValue) {
+    return ENV.get(key) != null ? getEnvAsList(key) : defaultValue;
+  }
+
+  /** Set an environment variable */
+  public static void setEnv(String key, String value) {
+    ENV.put(key, value);
+  }
+
+  /** Set all environment variables */
+  public static synchronized void setEnvs(Map<String, String> env) {
+    ENV.clear();
+    ENV.putAll(env);
+  }
+
+  /** Get all Solr system properties as a sorted map */
+  public static SortedMap<String, String> getProps() {
+    return System.getProperties().entrySet().stream()
+        .collect(
+            Collectors.toMap(
+                entry -> entry.getKey().toString(),
+                entry -> entry.getValue().toString(),
+                (e1, e2) -> e1,
+                TreeMap::new));
+  }
+
+  /** Get a property as string */
+  public static String getProp(String key) {
+    return getProp(key, null);
+  }
+
+  /**
+   * Get a property as string with a fallback value. All other getProp* methods use this.
+   *
+   * @param key property key, which treats 'camelCase' the same as 'camel.case'
+   * @param defaultValue fallback value if property is not found
+   */
+  public static String getProp(String key, String defaultValue) {
+    String value = getPropWithCamelCaseFallback(key);
+    return value != null ? value : defaultValue;
+  }
+
+  /**
+   * Get a property from given key or an alias key converted from CamelCase to dot separated.
+   *
+   * @return property value or value of dot-separated alias key or null if not found
+   */
+  private static String getPropWithCamelCaseFallback(String key) {
+    String value = System.getProperty(key);
+    if (value != null) {
+      return value;
+    } else {
+      // Figure out if string is CamelCase and convert to dot separated
+      String altKey = camelCaseToDotSeparated(key);
+      return System.getProperty(altKey);
+    }
+  }
+
+  private static String camelCaseToDotSeparated(String key) {
+    if (camelCaseToDotsMap.containsKey(key)) {
+      return camelCaseToDotsMap.get(key);
+    } else {
+      String converted =
+          String.join(".", key.split("(?=[A-Z])")).replace("..", ".").toLowerCase(Locale.ROOT);
+      camelCaseToDotsMap.put(key, converted);
+      return converted;
+    }
+  }
+
+  /** Get property as integer */
+  public static Long getPropAsLong(String key) {
+    return getPropAsLong(key, null);
+  }
+
+  /** Get property as long, or default value */
+  public static Long getPropAsLong(String key, Long defaultValue) {
+    String value = getProp(key);
+    if (value == null) {
+      return defaultValue;
+    }
+    return Long.parseLong(value);
+  }
+
+  /** Get property as boolean */
+  public static Boolean getPropAsBool(String key) {
+    return getPropAsBool(key, null);
+  }
+
+  /** Get property as boolean, or default value */
+  public static Boolean getPropAsBool(String key, Boolean defaultValue) {
+    String value = getProp(key);
+    if (value == null) {
+      return defaultValue;
+    }
+    return StrUtils.parseBool(value);
+  }
+
+  /**
+   * Get comma separated strings from sysprop as List
+   *
+   * @return list of strings, or null if not found
+   */
+  public static List<String> getPropAsList(String key) {
+    return getPropAsList(key, null);
+  }
+
+  /**
+   * Get comma separated strings from sysprop as List, or default value
+   *
+   * @return list of strings, or provided default if not found
+   */
+  public static List<String> getPropAsList(String key, List<String> defaultValue) {
+    return getProp(key) != null ? stringValueToList(getProp(key)) : defaultValue;
+  }
+
+  /** Set a system property. Shim to {@link System#setProperty(String, String)} */
+  public static void setProp(String key, String value) {
+    System.setProperty(key, value);
+    System.setProperty(camelCaseToDotSeparated(key), value);
+  }
+
+  /**
+   * Re-reads environment variables and updates the internal map. Mainly for internal and test use.
+   *
+   * @param overwrite if true, overwrite existing system properties with environment variables
+   */
+  static synchronized void init(boolean overwrite) {
+    // Convert eligible environment variables to system properties
+    for (String key : ENV.keySet()) {
+      if (key.startsWith("SOLR_") || CUSTOM_MAPPINGS.containsKey(key)) {
+        String sysPropKey = envNameToSyspropName(key);
+        // Existing system properties take precedence
+        if (!sysPropKey.isBlank() && (overwrite || getProp(sysPropKey, null) == null)) {
+          setProp(sysPropKey, ENV.get(key));
+        }
+      }
+    }
+  }
+
+  protected static String envNameToSyspropName(String envName) {
+    return CUSTOM_MAPPINGS.containsKey(envName)
+        ? CUSTOM_MAPPINGS.get(envName)
+        : envName.toLowerCase(Locale.ROOT).replace("_", ".");
+  }
+
+  /**
+   * Convert a string to a List&lt;String&gt;. If the string is a JSON array, it will be parsed as
+   * such. String splitting uses "splitSmart" which supports backslash escaped characters.
+   */
+  @SuppressWarnings("unchecked")
+  private static List<String> stringValueToList(String string) {
+    if (string.startsWith("[") && string.endsWith("]")) {
+      // Convert a JSON string to a List<String> using Noggit parser
+      return (List<String>) Utils.fromJSONString(string);
+    } else {
+      return StrUtils.splitSmart(string, ",", true).stream()
+          .map(String::trim)
+          .collect(Collectors.toList());
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/util/ModuleUtils.java b/solr/core/src/java/org/apache/solr/util/ModuleUtils.java
index 5c0ef36604b..4506b00ea3e 100644
--- a/solr/core/src/java/org/apache/solr/util/ModuleUtils.java
+++ b/solr/core/src/java/org/apache/solr/util/ModuleUtils.java
@@ -22,7 +22,6 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -56,17 +55,7 @@ public class ModuleUtils {
    * @return set of raw volume names from sysprop and/or env.var
    */
   static Set<String> resolveFromSyspropOrEnv() {
-    // Fall back to sysprop and env.var if nothing configured through solr.xml
-    Set<String> mods = new HashSet<>();
-    String modulesFromProps = System.getProperty("solr.modules");
-    if (StrUtils.isNotNullOrEmpty(modulesFromProps)) {
-      mods.addAll(StrUtils.splitSmart(modulesFromProps, ',', true));
-    }
-    String modulesFromEnv = System.getenv("SOLR_MODULES");
-    if (StrUtils.isNotNullOrEmpty(modulesFromEnv)) {
-      mods.addAll(StrUtils.splitSmart(modulesFromEnv, ',', true));
-    }
-    return mods.stream().map(String::trim).collect(Collectors.toSet());
+    return Set.copyOf(EnvUtils.getPropAsList("solr.modules", Collections.emptyList()));
   }
 
   /** Returns true if a module name is valid and exists in the system */
diff --git a/solr/core/src/java/org/apache/solr/util/StartupLoggingUtils.java b/solr/core/src/java/org/apache/solr/util/StartupLoggingUtils.java
index aa2fcd5d323..859c08e3452 100644
--- a/solr/core/src/java/org/apache/solr/util/StartupLoggingUtils.java
+++ b/solr/core/src/java/org/apache/solr/util/StartupLoggingUtils.java
@@ -44,7 +44,7 @@ public final class StartupLoggingUtils {
 
   /** Checks whether mandatory log dir is given */
   public static void checkLogDir() {
-    if (System.getProperty("solr.log.dir") == null) {
+    if (EnvUtils.getProp("solr.log.dir") == null) {
       log.error("Missing Java Option solr.log.dir. Logging may be missing or incomplete.");
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/util/circuitbreaker/CircuitBreakerRegistry.java b/solr/core/src/java/org/apache/solr/util/circuitbreaker/CircuitBreakerRegistry.java
index 14e9ee2bb47..b1782867cce 100644
--- a/solr/core/src/java/org/apache/solr/util/circuitbreaker/CircuitBreakerRegistry.java
+++ b/solr/core/src/java/org/apache/solr/util/circuitbreaker/CircuitBreakerRegistry.java
@@ -34,6 +34,7 @@ import java.util.stream.Collectors;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.util.EnvUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,8 +69,8 @@ public class CircuitBreakerRegistry implements Closeable {
   private static void initGlobal(CoreContainer coreContainer) {
     // Read system properties to register global circuit breakers for update and query:
     // Example: solr.circuitbreaker.update.cpu = 50
-    System.getProperties().keySet().stream()
-        .map(k -> SYSPROP_REGEX.matcher(k.toString()))
+    EnvUtils.getProps().keySet().stream()
+        .map(SYSPROP_REGEX::matcher)
         .filter(Matcher::matches)
         .collect(Collectors.groupingBy(m -> m.group(2) + ":" + System.getProperty(m.group(0))))
         .forEach(
diff --git a/solr/core/src/resources/EnvToSyspropMappings.properties b/solr/core/src/resources/EnvToSyspropMappings.properties
new file mode 100644
index 00000000000..fb51bf34028
--- /dev/null
+++ b/solr/core/src/resources/EnvToSyspropMappings.properties
@@ -0,0 +1,97 @@
+# Mapping from Environment variable to system property
+# This file only contains non-standard mappings that do not follow the standard naming convention
+# Map to nothing to avoid setting any system property for the env.variable
+# CamelCase properties are mapped to dot separated lowercase
+# This way, env SOLR_FOO_BAR will also match property 'solr.foo.bar' without a mapping in this file
+# TODO: Deprecate non-standard sysprops and standardize on solr.foo.bar in Solr 10
+AWS_PROFILE=aws.profile
+DEFAULT_CONFDIR=solr.default.confdir
+SOLR_ADMIN_UI_DISABLED=disableAdminUI
+SOLR_ALWAYS_ON_TRACE_ID=solr.alwaysOnTraceId
+SOLR_AUTH_JWT_ALLOW_OUTBOUND_HTTP=solr.auth.jwt.allowOutboundHttp
+SOLR_CONFIG_SET_FORBIDDEN_FILE_TYPES=solrConfigSetForbiddenFileTypes
+SOLR_DELETE_UNKNOWN_CORES=solr.deleteUnknownCores
+SOLR_DISABLE_REQUEST_ID=solr.disableRequestId
+SOLR_ENABLE_PACKAGES=enable.packages
+SOLR_ENABLE_REMOTE_STREAMING=solr.enableRemoteStreaming
+SOLR_ENABLE_STREAM_BODY=solr.enableStreamBody
+SOLR_HADOOP_CREDENTIAL_PROVIDER_PATH=hadoop.security.credential.provider.path
+SOLR_HIDDEN_SYS_PROPS=solr.hiddenSysProps
+SOLR_HOME=solr.solr.home
+SOLR_HOST=host
+SOLR_HTTP_DISABLE_COOKIES=solr.http.disableCookies
+SOLR_IP_ALLOWLIST=solr.jetty.inetaccess.includes
+SOLR_IP_DENYLIST=solr.jetty.inetaccess.excludes
+SOLR_LOGS_DIR=solr.log.dir
+SOLR_OTEL_DEFAULT_CONFIGURATOR=solr.otelDefaultConfigurator
+SOLR_PORT=jetty.port
+SOLR_TIMEZONE=user.timezone
+SOLR_TIP=solr.install.dir
+SOLR_TIP_SYM=solr.install.symDir
+SOLR_WAIT_FOR_ZK=waitForZk
+ZK_CLIENT_TIMEOUT=zkClientTimeout
+ZK_CREATE_CHROOT=createZkChroot
+ZK_CREDENTIALS_INJECTOR=zkCredentialsInjector
+ZK_CREDENTIALS_PROVIDER=zkCredentialsProvider
+ZK_DIGEST=PASSWORD=zkDigestPassword
+ZK_DIGEST_CREDENTIALS_FILE=zkDigestCredentialsFile
+ZK_DIGEST_READONLY_PASSWORD=zkDigestReadonlyPassword
+ZK_DIGEST_READONLY_USERNAME=zkDigestReadonlyUsername
+ZK_DIGEST_USERNAME=zkDigestUsername
+ZK_HOST=zkHost
+
+# Commonly used in solr.xml
+SOLR_ALLOW_PATHS=solr.allowPaths
+SOLR_ALLOW_URLS=solr.allowUrls
+SOLR_MAX_BOOLEAN_CLAUSES=solr.max.booleanClauses
+SOLR_METRICS_ENABLED=metricsEnabled
+SOLR_SHARED_LIB=solr.sharedLib
+SOLR_ZK_ACL_PROVIDER=zkACLProvider
+SOLR_ZK_CLIENT_TIMEOUT=solr.zkclienttimeout
+SOLR_ZK_CREDENTIALS_INJECTOR=zkCredentialsInjector
+SOLR_ZK_CREDENTIALS_PROVIDER=zkCredentialsProvider
+
+# Commonly used in solrconfig.xml
+SOLR_AUTO_SOFT_COMMIT_MAX_TIME=solr.autoSoftCommit.maxTime
+SOLR_AUTO_COMMIT_MAX_TIME=solr.autoCommit.maxTime
+SOLR_COMMITWITHIN_SOFTCOMMIT=solr.commitwithin.softcommit
+SOLR_DIRECTORY_FACTORY=solr.directoryFactory
+
+# These should not be mapped to system properties
+CLOUD_MODE_OPTS=
+SOLR_ADDL_ARGS=
+SOLR_AUTHENTICATION_CLIENT_BUILDER=
+SOLR_AUTHENTICATION_OPTS=
+SOLR_HEAP=
+SOLR_HEAP_DUMP=
+SOLR_HEAP_DUMP_DIR=
+SOLR_INCLUDE=
+SOLR_JAVA_MEM=
+SOLR_JAVA_STACK_SIZE=
+SOLR_JETTY_CONFIG=
+SOLR_OPTS=
+SOLR_OPTS_INTERNAL=
+SOLR_REQUESTLOG_ENABLED=
+SOLR_SECURITY_MANAGER_ENABLED=
+SOLR_SERVER_DIR=
+SOLR_SSL_CHECK_PEER_NAME=
+SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION=
+SOLR_SSL_CLIENT_KEY_STORE=
+SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=
+SOLR_SSL_CLIENT_KEY_STORE_TYPE=
+SOLR_SSL_CLIENT_TRUST_STORE=
+SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=
+SOLR_SSL_CLIENT_TRUST_STORE_TYPE=
+SOLR_SSL_ENABLED=
+SOLR_SSL_KEY_STORE=
+SOLR_SSL_KEY_STORE_PASSWORD=
+SOLR_SSL_KEY_STORE_TYPE=
+SOLR_SSL_NEED_CLIENT_AUTH=
+SOLR_SSL_OPTS=
+SOLR_SSL_TRUST_STORE=
+SOLR_SSL_TRUST_STORE_PASSWORD=
+SOLR_SSL_TRUST_STORE_TYPE=
+SOLR_SSL_WANT_CLIENT_AUTH=
+SOLR_START_OPTS=
+SOLR_START_WAIT=
+SOLR_STOP_WAIT=
diff --git a/solr/core/src/test/org/apache/solr/util/EnvUtilsTest.java b/solr/core/src/test/org/apache/solr/util/EnvUtilsTest.java
new file mode 100644
index 00000000000..19e1d2431c0
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/util/EnvUtilsTest.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.util;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.SolrTestCase;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class EnvUtilsTest extends SolrTestCase {
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    // Make a map of some common Solr environment variables for testing, and initialize EnvUtils
+    EnvUtils.setEnvs(
+        Map.of(
+            "SOLR_HOME", "/home/solr",
+            "SOLR_PORT", "8983",
+            "SOLR_HOST", "localhost",
+            "SOLR_LOG_LEVEL", "INFO",
+            "SOLR_BOOLEAN", "true",
+            "SOLR_LONG", "1234567890",
+            "SOLR_COMMASEP", "one,two, three",
+            "SOLR_JSON_LIST", "[\"one\", \"two\", \"three\"]",
+            "SOLR_ALWAYS_ON_TRACE_ID", "true",
+            "SOLR_STR_WITH_NEWLINE", "foo\nbar,baz"));
+    EnvUtils.init(true);
+  }
+
+  @Test
+  public void testGetEnv() {
+    assertEquals("INFO", EnvUtils.getEnv("SOLR_LOG_LEVEL"));
+
+    assertNull(EnvUtils.getEnv("SOLR_NONEXIST"));
+    assertEquals("myString", EnvUtils.getEnv("SOLR_NONEXIST", "myString"));
+
+    assertTrue(EnvUtils.getEnvAsBool("SOLR_BOOLEAN"));
+    assertFalse(EnvUtils.getEnvAsBool("SOLR_BOOLEAN_NONEXIST", false));
+
+    assertEquals("1234567890", EnvUtils.getEnv("SOLR_LONG"));
+    assertEquals(1234567890L, EnvUtils.getEnvAsLong("SOLR_LONG"));
+    assertEquals(987L, EnvUtils.getEnvAsLong("SOLR_LONG_NONEXIST", 987L));
+
+    assertEquals("one,two, three", EnvUtils.getEnv("SOLR_COMMASEP"));
+    assertEquals(List.of("one", "two", "three"), EnvUtils.getEnvAsList("SOLR_COMMASEP"));
+    assertEquals(List.of("one", "two", "three"), EnvUtils.getEnvAsList("SOLR_JSON_LIST"));
+    assertEquals(List.of("fallback"), EnvUtils.getEnvAsList("SOLR_MISSING", List.of("fallback")));
+    assertEquals(List.of("foo\nbar", "baz"), EnvUtils.getEnvAsList("SOLR_STR_WITH_NEWLINE"));
+  }
+
+  @Test
+  public void testGetProp() {
+    assertEquals("INFO", EnvUtils.getProp("solr.log.level"));
+
+    assertNull(EnvUtils.getProp("solr.nonexist"));
+    assertEquals("myString", EnvUtils.getProp("solr.nonexist", "myString"));
+
+    assertTrue(EnvUtils.getPropAsBool("solr.boolean"));
+    assertFalse(EnvUtils.getPropAsBool("solr.boolean.nonexist", false));
+
+    assertEquals("1234567890", EnvUtils.getProp("solr.long"));
+    assertEquals(Long.valueOf(1234567890L), EnvUtils.getPropAsLong("solr.long"));
+    assertEquals(Long.valueOf(987L), EnvUtils.getPropAsLong("solr.long.nonexist", 987L));
+
+    assertEquals("one,two, three", EnvUtils.getProp("solr.commasep"));
+    assertEquals(List.of("one", "two", "three"), EnvUtils.getPropAsList("solr.commasep"));
+    assertEquals(List.of("one", "two", "three"), EnvUtils.getPropAsList("solr.json.list"));
+    assertEquals(List.of("fallback"), EnvUtils.getPropAsList("SOLR_MISSING", List.of("fallback")));
+  }
+
+  @Test
+  public void getPropWithCamelCase() {
+    assertEquals("INFO", EnvUtils.getProp("solr.logLevel"));
+    assertEquals("INFO", EnvUtils.getProp("solr.LogLevel"));
+    assertEquals(Long.valueOf(1234567890L), EnvUtils.getPropAsLong("solrLong"));
+    assertEquals(Boolean.TRUE, EnvUtils.getPropAsBool("solr.alwaysOnTraceId"));
+    assertEquals(Boolean.TRUE, EnvUtils.getPropAsBool("solr.always.on.trace.id"));
+  }
+
+  @Test
+  public void testEnvsWithCustomKeyNameMappings() {
+    // These have different names than the environment variables
+    assertEquals(EnvUtils.getEnv("SOLR_HOME"), EnvUtils.getProp("solr.solr.home"));
+    assertEquals(EnvUtils.getEnv("SOLR_PORT"), EnvUtils.getProp("jetty.port"));
+    assertEquals(EnvUtils.getEnv("SOLR_HOST"), EnvUtils.getProp("host"));
+    assertEquals(EnvUtils.getEnv("SOLR_LOGS_DIR"), EnvUtils.getProp("solr.log.dir"));
+  }
+
+  @Test
+  public void testNotMapped() {
+    assertFalse(EnvUtils.getProps().containsKey("solr.ssl.key.store.password"));
+    assertFalse(EnvUtils.getProps().containsKey("gc.log.opts"));
+  }
+
+  @Test
+  public void testOverwrite() {
+    EnvUtils.setProp("solr.overwrite", "original");
+    EnvUtils.setEnv("SOLR_OVERWRITE", "overwritten");
+    EnvUtils.init(false);
+    assertEquals("original", EnvUtils.getProp("solr.overwrite"));
+    EnvUtils.init(true);
+    assertEquals("overwritten", EnvUtils.getProp("solr.overwrite"));
+  }
+}
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/property-substitution.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/property-substitution.adoc
index 21c6cebd247..752ae1a7f76 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/property-substitution.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/property-substitution.adoc
@@ -29,9 +29,7 @@ Of those below, strongly consider "config overlay" as the preferred approach, as
 
 == JVM System Properties
 
-Any JVM system property, usually specified using the `-D` flag when starting the JVM, can be used as variables in any XML configuration file in Solr.
-
-For example, in the sample `solrconfig.xml` files, you will see this value which defines the locking type to use:
+Any JVM system property can be used as variables in any XML configuration file in Solr. For example, in the sample `solrconfig.xml` files, you will see this value which defines the locking type to use:
 
 [source,xml]
 ----
@@ -47,6 +45,8 @@ bin/solr start -Dsolr.lock.type=none
 
 In general, any Java system property that you want to set can be passed through the `bin/solr` script using the standard `-Dproperty=value` syntax.
 
+Solr will also automatically map any environment variables that start with `SOLR_` to system properties by lowercasing the name and replacing underscores with `.`. This means that starting Solr with `SOLR_LOCK_TYPE=none` (or setting it in `solr.in.sh` or `solr.in.cmd`) will have the same effect as the previous example.
+
 Alternatively, you can add common system properties to the `SOLR_OPTS` environment variable defined in the Solr include file (`bin/solr.in.sh` or `bin/solr.in.cmd`).
 For more information about how the Solr include file works, refer to: xref:deployment-guide:taking-solr-to-production.adoc[].
 
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
index e4de26a87ad..3667da302a1 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
@@ -185,7 +185,7 @@ A token's 'aud' claim must match 'aud' for one of the configured issuers.
 === Using non-SSL URLs
 In production environments you should always use SSL protected HTTPS connections, otherwise you open yourself up to attacks.
 However, in development, it may be useful to use regular HTTP URLs, and bypass the security check that Solr performs.
-To support this you can set the environment variable `-Dsolr.auth.jwt.allowOutboundHttp=true` at startup.
+To support this you can set the system property `-Dsolr.auth.jwt.allowOutboundHttp=true` at startup.
 
 === Trusting the IdP server
 All communication with the Oauth2 server (IdP) is done over HTTPS.
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc
index 88d3a192ef6..dc580c73590 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc
@@ -301,6 +301,12 @@ For example, to set the auto soft-commit frequency to 3 seconds, you can do:
 
 `bin/solr start -Dsolr.autoSoftCommit.maxTime=3000`
 
+Solr will also convert any environment variable on the format `SOLR_FOO_BAR` to
+system property `solr.foo.bar`, making it possible to inject most properties
+through the environment, e.g:
+
+`SOLR_LOG_LEVEL=debug bin/solr start`
+
 The `SOLR_OPTS` environment variable is also available to set additional System Properties for Solr.
 
 In order to set custom System Properties when running any Solr utility other than `start` (e.g. `stop`, `create`, `auth`, `status`, `api`),
diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
index 0b3060d16d7..ee63315aefb 100644
--- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
+++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
@@ -74,6 +74,9 @@ Due to changes in Lucene 9, that isn't possible any more.
 === Global Circuit Breakers
 * Circuit breakers can now be configured globally, not only per collection. See xref:deployment-guide:circuit-breakers.adoc[Configuring Circuit Breakers] for more information.
 
+=== Environment variables and syste properties
+* Solr will now automatically resolve all environment variables with `SOLR_` prefix, and set the corresponding system property. This is useful for configuring more aspects of Solr through environment variables, such as for containers. Underscores are replaced with dots and strings are lowercased. For example, while you earlier had to set the system property `-Dsolr.clustering.enabled=true` to enable clustering, you can now set the equivalent environment variable `SOLR_CLUSTERING_ENABLED= [...]
+
 == Solr 9.4
 === The Built-In Config Sets
 * The build in ConfigSets (`_default` and `sample_techproducts_configs`), now use a default `autoSoftCommit` time of 3 seconds,
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java b/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java
index d9566648bf8..f722ea1b660 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCase.java
@@ -35,6 +35,7 @@ import org.apache.lucene.tests.util.QuickPatchThreadsFilter;
 import org.apache.lucene.tests.util.VerifyTestClassNamingConvention;
 import org.apache.solr.common.util.ObjectReleaseTracker;
 import org.apache.solr.servlet.SolrDispatchFilter;
+import org.apache.solr.util.EnvUtils;
 import org.apache.solr.util.ExternalPaths;
 import org.apache.solr.util.RevertDefaultThreadHandlerRule;
 import org.apache.solr.util.StartupLoggingUtils;
@@ -121,7 +122,7 @@ public class SolrTestCase extends LuceneTestCase {
   @BeforeClass
   public static void beforeSolrTestCase() {
     final String existingValue =
-        System.getProperty(SolrDispatchFilter.SOLR_DEFAULT_CONFDIR_ATTRIBUTE);
+        EnvUtils.getProp(SolrDispatchFilter.SOLR_DEFAULT_CONFDIR_ATTRIBUTE);
     if (null != existingValue) {
       log.info(
           "Test env includes configset dir system property '{}'='{}'",