如何从Java中设置环境变量?我发现我可以使用ProcessBuilder为子流程做到这一点。不过,我有几个子流程要启动,所以我宁愿修改当前流程的环境,让子流程继承它。

有一个System.getenv(String)用于获取单个环境变量。我还可以使用System.getenv()获得完整环境变量集的Map。但是,在该Map上调用put()会抛出UnsupportedOperationException——显然,它们意味着环境是只读的。并且,没有System.setenv()。

那么,有没有办法在当前运行的进程中设置环境变量呢?如果有,怎么做?如果不是,理由是什么?(是不是因为这是Java,所以我不应该做邪恶的、不可移植的、过时的事情,比如触摸我的环境?)如果不是,有什么好的建议来管理我将需要提供给几个子流程的环境变量更改吗?


当前回答

只需使用反射,比如setEnv("k1","v1")

    private void setEnv(String key, String val) throws Exception {
        getModifiableEnv().put(key, val);
    }
    
    private Map<String, String> getModifiableEnv() throws Exception {
        Map<String, String> unmodifiableEnv = System.getenv();
        Field field = unmodifiableEnv.getClass().getDeclaredField("m");
        field.setAccessible(true);
        return (Map<String, String>) field.get(unmodifiableEnv);
    }

need

import java.lang.reflect.Field;
import java.util.Map;

其他回答

蒂姆·瑞安的回答对我很管用……但我希望它用于Groovy(例如Spock上下文),并且simplissimo:

import java.lang.reflect.Field

def getModifiableEnvironmentMap() {
    def unmodifiableEnv = System.getenv()
    Class cl = unmodifiableEnv.getClass()
    Field field = cl.getDeclaredField("m")
    field.accessible = true
    field.get(unmodifiableEnv)
}

def clearEnvironmentVars( def keys ) {
    def savedVals = [:]
    keys.each{ key ->
        String val = modifiableEnvironmentMap.remove(key)
        // thinking about it, I'm not sure why we need this test for null
        // but haven't yet done any experiments
        if( val != null ) {
            savedVals.put( key, val )
        }
    }
    savedVals
}

def setEnvironmentVars(Map varMap) {
    modifiableEnvironmentMap.putAll(varMap)
}

// pretend existing Env Var doesn't exist
def PATHVal1 = System.env.PATH
println "PATH val1 |$PATHVal1|"
String[] keys = ["PATH", "key2", "key3"]
def savedVars = clearEnvironmentVars(keys)
def PATHVal2 = System.env.PATH
println "PATH val2 |$PATHVal2|"

// return to reality
setEnvironmentVars(savedVars)
def PATHVal3 = System.env.PATH
println "PATH val3 |$PATHVal3|"
println "System.env |$System.env|"

// pretend a non-existent Env Var exists
setEnvironmentVars( [ 'key4' : 'key4Val' ])
println "key4 val |$System.env.key4|"

在需要为单元测试设置特定环境值的场景中使用,您可能会发现以下hack很有用。它将在JVM中更改环境变量(因此请确保在测试后重置所有更改),但不会更改系统环境。

我发现Edward Campbell和anonymous的两个肮脏的黑客组合效果最好,因为一个在linux下不起作用,另一个在windows 7下也不起作用。所以为了获得一个多平台的邪恶黑客,我将它们结合起来:

protected static void setEnv(Map<String, String> newenv) throws Exception {
  try {
    Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
    Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
    theEnvironmentField.setAccessible(true);
    Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
    env.putAll(newenv);
    Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
    theCaseInsensitiveEnvironmentField.setAccessible(true);
    Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
    cienv.putAll(newenv);
  } catch (NoSuchFieldException e) {
    Class[] classes = Collections.class.getDeclaredClasses();
    Map<String, String> env = System.getenv();
    for(Class cl : classes) {
      if("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
        Field field = cl.getDeclaredField("m");
        field.setAccessible(true);
        Object obj = field.get(env);
        Map<String, String> map = (Map<String, String>) obj;
        map.clear();
        map.putAll(newenv);
      }
    }
  }
}

这招很管用。完全归功于这些黑客的两位作者。

有三个库可以在单元测试期间做到这一点。

有Stefan Birkner的系统规则和系统Lambda - https://www.baeldung.com/java-system-rules-junit,允许你做一些事情:

public class JUnitTest {

    @Rule
    public EnvironmentVariables environmentVariables = new EnvironmentVariables();

    @Test
    public void someTest() {
        environmentVariables.set("SOME_VARIABLE", "myValue");
        
        // now System.getenv does what you want
    }
}

或System-Lambda:

@Test
void execute_code_with_environment_variables(
) throws Exception {
  List<String> values = withEnvironmentVariable("first", "first value")
    .and("second", "second value")
    .execute(() -> asList(
      System.getenv("first"),
      System.getenv("second")
    ));
  assertEquals(
    asList("first value", "second value"),
    values
  );
}

上述功能也可以通过系统存根作为JUnit 5扩展:

@ExtendWith(SystemStubsExtension.class)
class SomeTest {

    @SystemStub
    private EnvironmentVariables;

    @Test
    void theTest() {
        environmentVariables.set("SOME_VARIABLE", "myValue");
        
        // now System.getenv does what you want

    }

}

系统存根向后兼容System Lambda和System Rules,但支持JUnit 5。

另外,还有JUnit Pioneer - https://github.com/junit-pioneer/junit-pioneer,它允许在测试时通过注释设置环境变量。

尝试了上面pushy的答案,它在很大程度上起了作用。然而,在某些情况下,我会看到这个例外:

java.lang.String cannot be cast to java.lang.ProcessEnvironment$Variable

由于ProcessEnvironment的某些内部类的实现,当该方法被多次调用时就会发生这种情况。如果setEnv(..)方法被多次调用,当从theEnvironment映射中检索键时,它们现在是字符串(在第一次调用setEnv(…)时已作为字符串放入),并且不能转换为映射的泛型类型Variable,这是ProcessEnvironment的私有内部类。

下面是一个固定版本(在Scala中)。希望将其应用到Java中并不太难。

def setEnv(newenv: java.util.Map[String, String]): Unit = {
  try {
    val processEnvironmentClass = JavaClass.forName("java.lang.ProcessEnvironment")
    val theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment")
    theEnvironmentField.setAccessible(true)

    val variableClass = JavaClass.forName("java.lang.ProcessEnvironment$Variable")
    val convertToVariable = variableClass.getMethod("valueOf", classOf[java.lang.String])
    convertToVariable.setAccessible(true)

    val valueClass = JavaClass.forName("java.lang.ProcessEnvironment$Value")
    val convertToValue = valueClass.getMethod("valueOf", classOf[java.lang.String])
    convertToValue.setAccessible(true)

    val sampleVariable = convertToVariable.invoke(null, "")
    val sampleValue = convertToValue.invoke(null, "")
    val env = theEnvironmentField.get(null).asInstanceOf[java.util.Map[sampleVariable.type, sampleValue.type]]
    newenv.foreach { case (k, v) => {
        val variable = convertToVariable.invoke(null, k).asInstanceOf[sampleVariable.type]
        val value = convertToValue.invoke(null, v).asInstanceOf[sampleValue.type]
        env.put(variable, value)
      }
    }

    val theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment")
    theCaseInsensitiveEnvironmentField.setAccessible(true)
    val cienv = theCaseInsensitiveEnvironmentField.get(null).asInstanceOf[java.util.Map[String, String]]
    cienv.putAll(newenv);
  }
  catch {
    case e : NoSuchFieldException => {
      try {
        val classes = classOf[java.util.Collections].getDeclaredClasses
        val env = System.getenv()
        classes foreach (cl => {
          if("java.util.Collections$UnmodifiableMap" == cl.getName) {
            val field = cl.getDeclaredField("m")
            field.setAccessible(true)
            val map = field.get(env).asInstanceOf[java.util.Map[String, String]]
            // map.clear() // Not sure why this was in the code. It means we need to set all required environment variables.
            map.putAll(newenv)
          }
        })
      } catch {
        case e2: Exception => e2.printStackTrace()
      }
    }
    case e1: Exception => e1.printStackTrace()
  }
}

我偶然发现了这个线程,因为我有一个类似的需求,我需要设置(或更新)一个环境变量(永久地)。

所以我研究了如何从命令提示符永久设置一个环境变量,这是非常简单的!

setx JAVA_LOC C:/Java/JDK

然后我在代码中实现了同样的功能 这是我使用的(假设- JAVA_LOC是env。变量名)

        String cmdCommand = "setx JAVA_LOC " + "C:/Java/JDK";
            ProcessBuilder processBuilder = new ProcessBuilder();
            processBuilder.command("cmd.exe", "/c", cmdCommand);
            processBuilder.start();

ProcessBuilder启动cmd.exe并传递您想要的命令。 即使您关闭JVM/重新启动系统,环境变量也会保留,因为它与JVM/程序的生命周期没有关系。