1. 概述

Java提供了与环境变量交互的简单方式,我们可以轻松读取它们,但修改却并不容易。然而在某些场景下(尤其是测试环境),我们需要对环境变量进行动态控制。

本文将探讨如何通过编程方式设置或修改环境变量。重点说明:本文仅讨论测试场景下的使用。在业务逻辑中滥用动态环境变量容易引发问题,应尽量避免。

2. 读取环境变量

读取环境变量的操作非常直接。System类提供了相关功能:

@Test
void givenOS_whenGetPath_thenVariableIsPresent() {
    String classPath = System.getenv("PATH");
    assertThat(classPath).isNotNull();
}

若需获取所有环境变量:

@Test
void givenOS_whenGetEnv_thenVariablesArePresent() {
    Map<String, String> environment = System.getenv();
    assertThat(environment).isNotNull();
}

⚠️ 注意:System类未提供修改方法,且返回的Map不可修改的

3. 修改环境变量

根据进程层级关系,修改环境变量有三种场景:

  • 子进程修改父进程的环境变量(不讨论)
  • 进程修改自身环境变量
  • 父进程修改子进程的环境变量

我们只关注后两种场景。第一种情况过于复杂且通常需要C/C++实现,本文仅讨论纯Java解决方案。

4. 修改当前进程环境变量

4.1. 使用反射API

通过反射可以强制修改System类的内部实现:

@SuppressWarnings("unchecked")
private static Map<String, String> getModifiableEnvironment()
  throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    Class<?> environmentClass = Class.forName(PROCESS_ENVIRONMENT);
    Field environmentField = environmentClass.getDeclaredField(ENVIRONMENT);
    assertThat(environmentField).isNotNull();
    environmentField.setAccessible(true);

    Object unmodifiableEnvironmentMap = environmentField.get(STATIC_METHOD);
    assertThat(unmodifiableEnvironmentMap).isNotNull();
    assertThat(unmodifiableEnvironmentMap).isInstanceOf(UMODIFIABLE_MAP_CLASS);

    Field underlyingMapField = unmodifiableEnvironmentMap.getClass().getDeclaredField(SOURCE_MAP);
    underlyingMapField.setAccessible(true);
    Object underlyingMap = underlyingMapField.get(unmodifiableEnvironmentMap);
    assertThat(underlyingMap).isNotNull();
    assertThat(underlyingMap).isInstanceOf(MAP_CLASS);

    return (Map<String, String>) underlyingMap;
}

踩坑提醒:此方法在Java 9+会破坏模块边界:

java.lang.reflect.InaccessibleObjectException: 
Unable to make field private static final java.util.Map java.lang.ProcessEnvironment.theUnmodifiableEnvironment accessible: 
module java.base does not "opens java.lang" to unnamed module @2c9f9fb0

需添加JVM参数解决:

--add-opens java.base/java.util=ALL-UNNAMED 
--add-opens java.base/java.lang=ALL-UNNAMED

推荐方案:使用JUnit Pioneer库简化操作:

<dependency>
    <groupId>org.junit-pioneer</groupId>
    <artifactId>junit-pioneer</artifactId>
    <version>2.2.0</version>
    <scope>test</scope>
</dependency>

声明式设置环境变量:

@Test
@SetEnvironmentVariable(key = ENV_VARIABLE_NAME, value = ENV_VARIABLE_VALUE)
void givenVariableSet_whenGetEnvironmentVariable_thenReturnsCorrectValue() {
    String actual = System.getenv(ENV_VARIABLE_NAME);
    assertThat(actual).isEqualTo(ENB_VARIABLE_VALUE);
}

4.2. JNI方案

通过JNI调用C/C++代码修改环境变量:

  • ⚠️ 缺点:需要C/C++技能,且可能无法更新Java运行时缓存
  • 跨平台问题:不同操作系统实现差异大
  • 优势:不涉及反射访问限制

5. 修改子进程环境变量

使用ProcessBuilder创建子进程并设置环境变量:

@Test
void givenChildProcessTestRunner_whenRunTheTest_thenAllSucceed()
  throws IOException, InterruptedException {
    ProcessBuilder processBuilder = new ProcessBuilder();
    processBuilder.inheritIO();

    Map<String, String> environment = processBuilder.environment();
    environment.put(CHILD_PROCESS_CONDITION, CHILD_PROCESS_VALUE);
    environment.put(ENVIRONMENT_VARIABLE_NAME, ENVIRONMENT_VALUE);
    Process process = processBuilder.command(arguments).start();

    int errorCode = process.waitFor();
    assertThat(errorCode).isZero();
}

通过Maven执行指定测试:

public static final String CHILD_PROCESS_TAG = "child_process";
public static final String TAG = String.format("-Dgroups=%s", CHILD_PROCESS_TAG);
private final String testClass = String.format("-Dtest=%s", getClass().getName());
private final String[] arguments = {"mvn", "test", TAG, testClass};

标记测试用例:

@Test
@EnabledIfEnvironmentVariable(named = CHILD_PROCESS_CONDITION, matches = CHILD_PROCESS_VALUE)
@Tag(CHILD_PROCESS_TAG)
void givenChildProcess_whenGetEnvironmentVariable_thenReturnsCorrectValue() {
    String actual = System.getenv(ENVIRONMENT_VARIABLE_NAME);
    assertThat(actual).isEqualTo(ENVIRONMENT_VALUE);
}

6. Docker环境方案

对于复杂测试环境,推荐使用Docker+Testcontainers:

Dockerfile示例:

FROM maven:3.9-amazoncorretto-17
WORKDIR /app
COPY /src/test/java/com/baeldung/setenvironment/SettingDockerEnvironmentVariableUnitTest.java \
 ./src/test/java/com/baeldung/setenvironment/
COPY /docker-pom.xml ./
ENV CUSTOM_DOCKER_ENV_VARIABLE=TRUE
ENTRYPOINT mvn -f docker-pom.xml test

Testcontainers集成示例:

class SettingTestcontainerVariableUnitTest {
    public static final String CONTAINER_REPORT_FILE = "/app/target/surefire-reports/TEST-com.baeldung.setenvironment.SettingDockerEnvironmentVariableUnitTest.xml";
    public static final String HOST_REPORT_FILE = "./container-test-report.xml";
    public static final String DOCKERFILE = "./Dockerfile";

    @Test
    void givenTestcontainerEnvironment_whenGetEnvironmentVariable_thenReturnsCorrectValue() {
        Path dockerfilePath = Paths.get(DOCKERFILE);
        GenericContainer container = new GenericContainer(
          new ImageFromDockerfile().withDockerfile(dockerfilePath));
        assertThat(container).isNotNull();
        container.start();
        while (container.isRunning()) {
            // Busy spin
        }
        container.copyFileFromContainer(CONTAINER_REPORT_FILE, HOST_REPORT_FILE);
    }
}

⚠️ 注意:容器文件复制功能有限,建议使用withFileSystemBind()或Dockerfile直接配置挂载。

7. 总结

Java允许直接操作环境变量,但动态修改存在挑战:

  • 测试场景:反射/子进程/Docker方案各有适用场景
  • 业务逻辑:滥用环境变量通常违反SOLID原则
  • 推荐方案:优先考虑子进程或Docker方案,反射方案需谨慎处理模块访问问题

所有示例代码可在GitHub仓库获取。


原始标题:Set an Environment Variable at Runtime in Java | Baeldung