如何从Java设置环境变量?

405

如何从Java设置环境变量?我发现可以使用ProcessBuilder为子进程完成此操作。但是,我需要启动多个子进程,所以我宁愿修改当前进程的环境并让子进程继承它。

可以使用System.getenv(String)获取单个环境变量,也可以使用System.getenv()获取完整的环境变量Map。但是,在该Map上调用put()会抛出UnsupportedOperationException异常--显然该环境被设计为只读。并且没有System.setenv()方法。

那么,在当前运行的进程中是否有任何设置环境变量的方法呢?如果有,怎么做?如果没有,原因是什么?(这是因为这是Java,因此我不应该执行像触摸环境之类的非便携式过时操作吗?)如果没有,有没有好的建议来管理我需要提供给多个子进程的环境变量更改?


1
System.getEnv() 旨在具有普遍性,但某些环境甚至没有环境变量。 - arkon
18
对于任何需要进行单元测试的情况,以下链接可能会有所帮助:https://dev59.com/jmsz5IYBdhLWcg3wADS6。 - Atif
1
对于Scala,请使用此链接:https://gist.github.com/vpatryshev/b1bbd15e2b759c157b58b68c58891ff4 - Vlad Patryshev
24个回答

275

如果你需要在单元测试中设置特定的环境变量值,可以使用下面这个技巧。它会更改整个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);
      }
    }
  }
}

这个方法非常有效。完全归功于这两个骇客的贡献。


51
这将仅更改内存中的环境变量。这对于测试很有用,因为您可以根据需要设置环境变量,但保留系统中的环境变量不变。实际上,我强烈不建议任何人将此代码用于除测试之外的任何其他目的。这段代码很邪恶;-) - pushy
15
请注意,JVM 在启动时会创建环境变量的副本。这将编辑该副本,而不是启动 JVM 的父进程的环境变量。 - bmeding
5
当然,import java.lang.reflect.Field; 的翻译是:导入 java.lang.reflect.Field - pushy
2
该方法不起作用,在jdk8中ProcessEnvironment没有theCaseInsensitiveEnvironment字段! - geosmart
2
在OpenJDK 17中不起作用。而且相应的实现在Linux和Windows上也有所不同。 - Ed Gomoliako
显示剩余16条评论

110

(这是因为这是Java,所以我不应该做像触及环境这样的邪恶的、非可移植的过时的事情吗?)

我认为你正中要害了。

减轻负担的一种可能的方法是将一个方法分解出来。

void setUpEnvironment(ProcessBuilder builder) {
    Map<String, String> env = builder.environment();
    // blah blah
}

在启动进程之前,请通过其传递任何ProcessBuilder

此外,您可能已经知道,但是您可以使用相同的ProcessBuilder启动多个进程。因此,如果您的子进程相同,则无需一遍又一遍地进行此设置。


23
S.Lott,我不是要设置父级环境,我是要设置自己的环境。 - skiphoppy
3
除非是其他人(例如Sun公司)启动该进程,否则这个方法效果很好。 - sullivan-
30
您错了。没有人可以操纵您的环境变量,因为这些变量是进程本地的(在Windows中设置的是默认值)。每个进程都可以自由更改自己的变量... 除了Java。 - maaartinus
1
微软的 .Net 拥有一种方法,允许修改当前进程的环境变量:http://msdn.microsoft.com/en-us/library/system.environment.setenvironmentvariable.aspx。在我的上一个项目中,它被用于自动配置专有库的路径。 - kevinarpe
19
Java 的这种限制有点像是推卸责任。除了“我们不想让 Java 这样做”之外,没有理由不允许您设置环境变量。 - IanNorton
显示剩余4条评论

92
public static void set(Map<String, String> newenv) throws Exception {
    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);
        }
    }
}

或者按照thejoshwolfe的建议,添加/更新单个变量并删除循环。

@SuppressWarnings({ "unchecked" })
  public static void updateEnv(String name, String val) throws ReflectiveOperationException {
    Map<String, String> env = System.getenv();
    Field field = env.getClass().getDeclaredField("m");
    field.setAccessible(true);
    ((Map<String, String>) field.get(env)).put(name, val);
  }

3
这句话的意思是,这个操作似乎会修改内存中的地图,但它是否会将值保存到系统中呢? - Jon Onstott
1
它确实改变了环境变量的内存映射。我想在很多用例中这就足够了。 @Edward - 哇,很难想象这个解决方案是如何首先被发现的! - anirvan
23
这不会改变系统环境变量,但会在当前 Java 调用中更改它们。这对单元测试非常有用。 - Stuart K
11
为什么不使用 Class<?> cl = env.getClass(); 而要使用那个循环呢? - thejoshwolfe
8
它在Java17上停止工作了。 - Gavriel
显示剩余3条评论

38

根据 Edward Campbell 的回答,设置单个环境变量:

public static void setEnv(String key, String value) {
    try {
        Map<String, String> env = System.getenv();
        Class<?> cl = env.getClass();
        Field field = cl.getDeclaredField("m");
        field.setAccessible(true);
        Map<String, String> writableEnv = (Map<String, String>) field.get(env);
        writableEnv.put(key, value);
    } catch (Exception e) {
        throw new IllegalStateException("Failed to set environment variable", e);
    }
}

使用方法:

首先,将该方法放在您想要的任何类中,例如SystemUtil。然后以静态方式调用它:

SystemUtil.setEnv("SHELL", "/bin/bash");

如果您在此之后调用System.getenv("SHELL"),您将得到"/bin/bash"返回。


以上在Windows 10中不起作用,但在Linux中会起作用。 - mengchengfeng
1
它在我的Windows 10和Linux上运行良好。恭喜! - Bastien Gallienne
1
为什么这只能在Linux/Mac上运行?显然Bash可能不会默认存在,但这里的Java代码并不依赖于平台。 - Matthew Read
4
它在Java17上停止工作了。 - Gavriel
以这种方式设置的变量是否会被子进程继承,就像原问题中指定的那样?如果它们确实可以被继承,我会感到惊讶,因为这似乎并没有修改进程的本机环境,只是Java解析的副本而已。 - undefined
显示剩余5条评论

24
// this is a dirty hack - but should be ok for a unittest.
private void setNewEnvironmentHack(Map<String, String> newenv) throws Exception
{
  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.clear();
  env.putAll(newenv);
  Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
  theCaseInsensitiveEnvironmentField.setAccessible(true);
  Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
  cienv.clear();
  cienv.putAll(newenv);
}

21
在Android系统中,接口通过 Libcore.os 以一种隐藏的API的形式暴露出来。
Libcore.os.setenv("VAR", "value", bOverwrite);
Libcore.os.getenv("VAR"));

Libcore类和OS接口都是公共的。只是缺少类声明并需要显示给链接器。不需要将这些类添加到应用程序中,但如果包含它们也不会有问题。

package libcore.io;

public final class Libcore {
    private Libcore() { }

    public static Os os;
}

package libcore.io;

public interface Os {
    public String getenv(String name);
    public void setenv(String name, String value, boolean overwrite) throws ErrnoException;
}

1
已在Android 4.4.4(CM11)上进行了测试并且可用。附注:我所做的唯一调整是将“throws ErrnoException”替换为“throws Exception”。 - DavisNT
8
API 21现在新增了Os.setEnv方法。该方法具体参见http://developer.android.com/reference/android/system/Os.html#setenv(java.lang.String, java.lang.String, boolean) - Jared Burrows
1
由于Pie的新限制,可能已经失效: https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces - TWiStErRob

12

这是@paul-blair的回答转换成Java代码的组合,其中包括paul blair指出的一些清理和似乎存在于@pushy的代码中的一些错误,@pushy的代码由@Edward Campbell和匿名人士组成。

我无法强调这段代码应该仅在测试中使用,并且极其hacky。但对于需要在测试中设置环境的情况,它正是我所需要的。

这还包括我做的一些小修改,使代码可以在运行Windows的计算机上工作。

java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

以及运行在 Centos 上的

openjdk version "1.8.0_91"
OpenJDK Runtime Environment (build 1.8.0_91-b14)
OpenJDK 64-Bit Server VM (build 25.91-b14, mixed mode)

实现:

/**
 * Sets an environment variable FOR THE CURRENT RUN OF THE JVM
 * Does not actually modify the system's environment variables,
 *  but rather only the copy of the variables that java has taken,
 *  and hence should only be used for testing purposes!
 * @param key The Name of the variable to set
 * @param value The value of the variable to set
 */
@SuppressWarnings("unchecked")
public static <K,V> void setenv(final String key, final String value) {
    try {
        /// we obtain the actual environment
        final Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
        final Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
        final boolean environmentAccessibility = theEnvironmentField.isAccessible();
        theEnvironmentField.setAccessible(true);

        final Map<K,V> env = (Map<K, V>) theEnvironmentField.get(null);

        if (SystemUtils.IS_OS_WINDOWS) {
            // This is all that is needed on windows running java jdk 1.8.0_92
            if (value == null) {
                env.remove(key);
            } else {
                env.put((K) key, (V) value);
            }
        } else {
            // This is triggered to work on openjdk 1.8.0_91
            // The ProcessEnvironment$Variable is the key of the map
            final Class<K> variableClass = (Class<K>) Class.forName("java.lang.ProcessEnvironment$Variable");
            final Method convertToVariable = variableClass.getMethod("valueOf", String.class);
            final boolean conversionVariableAccessibility = convertToVariable.isAccessible();
            convertToVariable.setAccessible(true);

            // The ProcessEnvironment$Value is the value fo the map
            final Class<V> valueClass = (Class<V>) Class.forName("java.lang.ProcessEnvironment$Value");
            final Method convertToValue = valueClass.getMethod("valueOf", String.class);
            final boolean conversionValueAccessibility = convertToValue.isAccessible();
            convertToValue.setAccessible(true);

            if (value == null) {
                env.remove(convertToVariable.invoke(null, key));
            } else {
                // we place the new value inside the map after conversion so as to
                // avoid class cast exceptions when rerunning this code
                env.put((K) convertToVariable.invoke(null, key), (V) convertToValue.invoke(null, value));

                // reset accessibility to what they were
                convertToValue.setAccessible(conversionValueAccessibility);
                convertToVariable.setAccessible(conversionVariableAccessibility);
            }
        }
        // reset environment accessibility
        theEnvironmentField.setAccessible(environmentAccessibility);

        // we apply the same to the case insensitive environment
        final Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
        final boolean insensitiveAccessibility = theCaseInsensitiveEnvironmentField.isAccessible();
        theCaseInsensitiveEnvironmentField.setAccessible(true);
        // Not entirely sure if this needs to be casted to ProcessEnvironment$Variable and $Value as well
        final Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
        if (value == null) {
            // remove if null
            cienv.remove(key);
        } else {
            cienv.put(key, value);
        }
        theCaseInsensitiveEnvironmentField.setAccessible(insensitiveAccessibility);
    } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        throw new IllegalStateException("Failed setting environment variable <"+key+"> to <"+value+">", e);
    } catch (final NoSuchFieldException e) {
        // we could not find theEnvironment
        final Map<String, String> env = System.getenv();
        Stream.of(Collections.class.getDeclaredClasses())
                // obtain the declared classes of type $UnmodifiableMap
                .filter(c1 -> "java.util.Collections$UnmodifiableMap".equals(c1.getName()))
                .map(c1 -> {
                    try {
                        return c1.getDeclaredField("m");
                    } catch (final NoSuchFieldException e1) {
                        throw new IllegalStateException("Failed setting environment variable <"+key+"> to <"+value+"> when locating in-class memory map of environment", e1);
                    }
                })
                .forEach(field -> {
                    try {
                        final boolean fieldAccessibility = field.isAccessible();
                        field.setAccessible(true);
                        // we obtain the environment
                        final Map<String, String> map = (Map<String, String>) field.get(env);
                        if (value == null) {
                            // remove if null
                            map.remove(key);
                        } else {
                            map.put(key, value);
                        }
                        // reset accessibility
                        field.setAccessible(fieldAccessibility);
                    } catch (final ConcurrentModificationException e1) {
                        // This may happen if we keep backups of the environment before calling this method
                        // as the map that we kept as a backup may be picked up inside this block.
                        // So we simply skip this attempt and continue adjusting the other maps
                        // To avoid this one should always keep individual keys/value backups not the entire map
                        LOGGER.info("Attempted to modify source map: "+field.getDeclaringClass()+"#"+field.getName(), e1);
                    } catch (final IllegalAccessException e1) {
                        throw new IllegalStateException("Failed setting environment variable <"+key+"> to <"+value+">. Unable to access field!", e1);
                    }
                });
    }
    LOGGER.info("Set environment variable <"+key+"> to <"+value+">. Sanity Check: "+System.getenv(key));
}

它有效,使用方式如下: setenv("coba","coba value"); System.out.println(System.getenv("coba"));结果: 将环境变量<coba>设置为<coba value>。检查:coba 值 coba value - Mang Jojot

10

原来@pushy/@anonymous/@Edward Campbell的解决方案在Android上不起作用,因为Android并非真正的Java。具体而言,Android根本没有java.lang.ProcessEnvironment。但是在Android上却更容易,你只需要进行一个JNI调用POSIX的setenv()函数:

在C/JNI中:

JNIEXPORT jint JNICALL Java_com_example_posixtest_Posix_setenv
  (JNIEnv* env, jclass clazz, jstring key, jstring value, jboolean overwrite)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = (char *) (*env)->GetStringUTFChars(env, value, NULL);
    int err = setenv(k, v, overwrite);
    (*env)->ReleaseStringUTFChars(env, key, k);
    (*env)->ReleaseStringUTFChars(env, value, v);
    return err;
}

而在Java中:

public class Posix {

    public static native int setenv(String key, String value, boolean overwrite);

    private void runTest() {
        Posix.setenv("LD_LIBRARY_PATH", "foo", true);
    }
}

9
像大多数找到这个主题的人一样,我正在编写一些单元测试,并需要修改环境变量以设置正确的条件来运行测试。然而,我发现最受欢迎的答案存在一些问题和/或非常神秘或过于复杂。希望这可以帮助其他人更快地解决方案。
首先,我最终发现@Hubert Grzeskowiak的解决方案是最简单的,并且对我有用。我希望我第一个就能想到那个。它基于@Edward Campbell的答案,但没有复杂的循环搜索。
然而,我从@pushy的解决方案开始,它获得了最多的投票。它是@anonymous和@Edward Campbell's的组合。@pushy声称两种方法都需要覆盖Linux和Windows环境。我在OS X下运行,发现两者都可以使用(一旦修复了@anonymous方法中的问题)。正如其他人所指出的,这个解决方案大多数时候有效,但并非全部。
我认为大部分混淆的源头来自@anonymous的解决方案操作“theEnvironment”字段。查看ProcessEnvironment结构的定义,“theEnvironment”不是Map< String, String >,而是Map< Variable, Value >。清除映射很好用,但putAll操作会重建一个Map< String, String >,这在后续操作使用期望Map<Variable, Value>的正常API的数据结构时可能会引起问题。此外,访问/删除单个元素也是一个问题。解决方案是间接地通过“theUnmodifiableEnvironment”访问“theEnvironment”。但由于这是类型UnmodifiableMap,因此必须通过UnmodifiableMap类型的私有变量'm'进行访问。请参见下面代码中的getModifiableEnvironmentMap2。
在我的情况下,我需要删除一些环境变量以进行测试(其他变量应保持不变)。然后我想在测试后将环境变量恢复到它们以前的状态。以下例程使此变得直截了当。我在OS X上测试了getModifiableEnvironmentMap的两个版本,两者都同样有效。尽管根据这个主题中的评论,其中一个可能比另一个更好,具体取决于环境。
注意:我没有包括对“theCaseInsensitiveEnvironmentField”的访问,因为那似乎只适用于Windows,而我无法测试它,但添加它应该很简单。
private Map<String, String> getModifiableEnvironmentMap() {
    try {
        Map<String,String> unmodifiableEnv = System.getenv();
        Class<?> cl = unmodifiableEnv.getClass();
        Field field = cl.getDeclaredField("m");
        field.setAccessible(true);
        Map<String,String> modifiableEnv = (Map<String,String>) field.get(unmodifiableEnv);
        return modifiableEnv;
    } catch(Exception e) {
        throw new RuntimeException("Unable to access writable environment variable map.");
    }
}

private Map<String, String> getModifiableEnvironmentMap2() {
    try {
        Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
        Field theUnmodifiableEnvironmentField = processEnvironmentClass.getDeclaredField("theUnmodifiableEnvironment");
        theUnmodifiableEnvironmentField.setAccessible(true);
        Map<String,String> theUnmodifiableEnvironment = (Map<String,String>)theUnmodifiableEnvironmentField.get(null);

        Class<?> theUnmodifiableEnvironmentClass = theUnmodifiableEnvironment.getClass();
        Field theModifiableEnvField = theUnmodifiableEnvironmentClass.getDeclaredField("m");
        theModifiableEnvField.setAccessible(true);
        Map<String,String> modifiableEnv = (Map<String,String>) theModifiableEnvField.get(theUnmodifiableEnvironment);
        return modifiableEnv;
    } catch(Exception e) {
        throw new RuntimeException("Unable to access writable environment variable map.");
    }
}

private Map<String, String> clearEnvironmentVars(String[] keys) {

    Map<String,String> modifiableEnv = getModifiableEnvironmentMap();

    HashMap<String, String> savedVals = new HashMap<String, String>();

    for(String k : keys) {
        String val = modifiableEnv.remove(k);
        if (val != null) { savedVals.put(k, val); }
    }
    return savedVals;
}

private void setEnvironmentVars(Map<String, String> varMap) {
    getModifiableEnvironmentMap().putAll(varMap);   
}

@Test
public void myTest() {
    String[] keys = { "key1", "key2", "key3" };
    Map<String, String> savedVars = clearEnvironmentVars(keys);

    // do test

    setEnvironmentVars(savedVars);
}

谢谢,这正是我需要的用例,而且还在 Mac OS X 下运行。 - Rafael Gonçalves
我非常喜欢这个,所以我为Groovy准备了一个稍微简单一些的版本,见下文。 - mike rodent
对于单元测试,我强烈建议使用其中一个能够为您完成此任务的测试库。有JUnit Pioneer、System Stubs、System Rules和System Lambda可供选择。 - undefined

8

有三个库可以在单元测试期间执行此操作。

其中 Stefan Birkner 的 System Rules 和 System 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
  );
}

上述功能也可通过System Stubs作为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 Stubs 兼容于 System Lambda 和 System Rules,但支持 JUnit 5。

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


JUnit Pioneer非常出色。在测试方法上加上一个简单的@ SetEnvironmentVariable(key =“SOME_VARIABLE”,value =“myValue”),您就可以开始运行了。 - Matthew Read
是的,但环境变量值必须在编码时知道,这对于设置诸如动态端口等内容不起作用。 - Ashley Frieze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接