如何从Java设置环境变量?

How do I set environment variables from Java?

如何从Java设置环境变量? 我看到我可以对使用ProcessBuilder的子流程执行此操作。 不过,我有几个子流程要启动,所以我宁愿修改当前流程的环境,并让这些子流程继承它。

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

那么,有什么方法可以在当前运行的进程中设置环境变量? 如果是这样,怎么办? 如果没有,有什么根据? (是因为这是Java,所以我不应该像触摸我的环境那样做邪恶的,不可移植的过时的事情吗?)如果没有,关于管理环境变量更改的任何好的建议将需要反馈给几个子进程?


对于需要为单元测试设置特定环境值的方案,您可能会发现以下技巧很有用。它会在整个JVM中更改环境变量(因此请确保在测试后重置所有更改),但不会更改系统环境。

我发现爱德华·坎贝尔和匿名的两个肮脏黑客的组合效果最好,因为其中一个在Linux下不起作用,一个在Windows 7下不起作用。因此,为了获得多平台邪恶的黑客,我将它们结合在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
      }
    }
  }
}

这就像一个魅力。完全归功于这些骇客的两位作者。


(Is it because this is Java and therefore I shouldn't be doing evil nonportable obsolete things like touching my environment?)

我认为您已经打中了头。

减轻负担的一种可能方法是排除一种方法

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

并在启动它们之前将所有ProcessBuilder传递给它。

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的建议添加/更新单个var并删除循环。

1
2
3
4
5
6
7
@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);
  }


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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);
}

在Android上,该接口通过Libcore.os作为一种隐藏的API公开。

1
2
Libcore.os.setenv("VAR","value", bOverwrite);
Libcore.os.getenv("VAR"));

Libcore类以及接口OS是公共的。仅缺少类声明,需要将其显示给链接器。无需将类添加到应用程序中,但是如果包含了类,也不会受到损害。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}


仅Linux

设置单个环境变量(基于Edward Campbell的回答):

1
2
3
4
5
6
7
8
9
10
11
12
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。

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

如果在此之后调用System.getenv("SHELL"),则会返回"/bin/bash"


事实证明,@ pushy / @ anonymous / @ Edward Campbell的解决方案在Android上不起作用,因为Android并不是真正的Java。具体来说,Android根本没有java.lang.ProcessEnvironment。但是事实证明,在Android中这更容易,您只需要对POSIX setenv()进行JNI调用:

在C / JNI中:

1
2
3
4
5
6
7
8
9
10
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中:

1
2
3
4
5
6
7
8
public class Posix {

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

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

这是@ paul-blair的答案转换为Java的组合,其中包括paul blair指出的一些清除以及似乎在@pushy的代码中的一些错误,这些错误由@Edward Campbell和匿名用户组成。

我无法强调此代码仅应在测试中使用多少,并且非常hacky。但是对于需要在测试中进行环境设置的情况,这正是我所需要的。

这还包括我的一些小改动,使代码可以在运行Windows的两个Windows上运行。

1
2
3
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正在运行

1
2
3
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)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
 * 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));
}

像大多数发现此线程的人一样,我正在编写一些单元测试,需要修改环境变量以设置正确的条件以运行测试。但是,我发现最受欢迎的答案存在一些问题,并且/或者非常含糊或过于复杂。希望这将帮助其他人更快地解决问题。

首先,我终于发现@Hubert Grzeskowiak的解决方案是最简单的,并且对我有用。我希望我先来谈那个。它基于@Edward Campbell的答案,但没有使循环搜索复杂化。

但是,我从@pushy的解决方案开始,该解决方案获得最多的支持。它是@anonymous和@Edward Campbell的组合。 @pushy声称,这两种方法都需要涵盖Linux和Windows环境。我在OS X上运行,发现两者都可以工作(一旦解决了@anonymous方法的问题)。正如其他人指出的那样,此解决方案在大多数情况下有效,但并非全部。

我认为大多数混淆的根源来自在'theEnvironment'字段上运行的@anonymous解决方案。查看ProcessEnvironment结构的定义," theEnvironment"不是Map ,而是Map 。清除地图可以正常工作,但是putAll操作将地图重新??构建为Map ,当后续操作使用期望Map 的常规API对数据结构进行操作时,这可能会导致问题。另外,访问/删除单个元素也是一个问题。解决方案是通过"不可修改的环境"间接访问"环境"。但是,由于这是UnmodifiableMap类型,因此必须通过UnmodifiableMap类型的私有变量'm'进行访问。请参见下面的代码中的getModifiableEnvironmentMap2。

就我而言,我需要为测试删除一些环境变量(其他变量应保持不变)。然后,我想在测试后将环境变量恢复到先前的状态。下面的例程可以帮助您做到这一点。我在OS X上测试了两个版本的getModifiableEnvironmentMap,并且两个版本均等效。尽管基于该线程中的注释,但根据环境,一个可能比另一个更好。

注意:我没有包括对" theCaseInsensitiveEnvironmentField"的访问,因为这似乎是Windows特定的,并且我无法对其进行测试,但是添加起来应该很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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);
}


在上面尝试了pushy的答案,并且在大多数情况下都有效。但是,在某些情况下,我会看到此异常:

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

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

下面是一个固定版本(在Scala中)。希望可以将它移植到Java中不太困难。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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()
  }
}


在网上闲逛,似乎可以使用JNI做到这一点。然后,您必须从C调用putenv(),并且(大概)必须在Windows和UNIX上都可以使用。

如果所有这些都能做到,那么Java本身就可以支持它,而不是直接穿上外套,肯定不会太难。

在其他地方讲Perl的朋友认为,这是因为环境变量是全局过程的,而Java则在努力为好的设计提供良好的隔离。


如果使用SpringBoot,则可以在以下属性中添加指定环境变量:

1
was.app.config.properties.toSystemProperties

这是@pushy的邪恶答案的Kotlin邪恶版本=)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Suppress("UNCHECKED_CAST")
@Throws(Exception::class)
fun setEnv(newenv: Map<String, String>) {
    try {
        val processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment")
        val theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment")
        theEnvironmentField.isAccessible = true
        val env = theEnvironmentField.get(null) as MutableMap<String, String>
        env.putAll(newenv)
        val theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment")
        theCaseInsensitiveEnvironmentField.isAccessible = true
        val cienv = theCaseInsensitiveEnvironmentField.get(null) as MutableMap<String, String>
        cienv.putAll(newenv)
    } catch (e: NoSuchFieldException) {
        val classes = Collections::class.java.getDeclaredClasses()
        val env = System.getenv()
        for (cl in classes) {
            if ("java.util.Collections\$UnmodifiableMap" == cl.getName()) {
                val field = cl.getDeclaredField("m")
                field.setAccessible(true)
                val obj = field.get(env)
                val map = obj as MutableMap<String, String>
                map.clear()
                map.putAll(newenv)
            }
        }
    }

至少在macOS Mojave中可以运行。


我最近根据爱德华的答案进行了Kotlin的实现:

1
2
3
4
5
6
7
8
9
10
11
fun setEnv(newEnv: Map<String, String>) {
    val unmodifiableMapClass = Collections.unmodifiableMap<Any, Any>(mapOf()).javaClass
    with(unmodifiableMapClass.getDeclaredField("m")) {
        isAccessible = true
        @Suppress("UNCHECKED_CAST")
        get(System.getenv()) as MutableMap<String, String>
    }.apply {
        clear()
        putAll(newEnv)
    }
}

您可以使用-D将参数传递到初始的Java进程中:

1
java -cp <classpath> -Dkey1=value -Dkey2=value ...