1. Overview
1.概述
Java provides a simple way of interacting with environment variables. We can access them but cannot change them easily. However, in some cases, we need more control over the environment variables, especially for test scenarios.
Java 提供了一种与环境变量交互的简单方法。我们可以访问它们,但不能轻易更改。不过,在某些情况下,我们需要对环境变量进行更多控制,尤其是在测试场景中。
In this tutorial, we’ll learn how to address this problem and programmatically set or change environment variables. We’ll be talking only about using it in a testing context. Using dynamic environment variables for domain logic should be discouraged, as it is prone to problems.
在本教程中,我们将学习如何解决这一问题,并以编程方式设置或更改环境变量。我们将仅讨论在测试上下文中使用它。应避免在域逻辑中使用动态环境变量,因为这样做很容易出现问题。
2. Accessing Environment Variables
2.访问环境变量
The process of accessing the environment variables is pretty straightforward. The System class provides us with such functionality:
访问环境变量的过程非常简单。System 类为我们提供了这种 功能:
@Test
void givenOS_whenGetPath_thenVariableIsPresent() {
String classPath = System.getenv("PATH");
assertThat(classPath).isNotNull();
}
Also, if we need to access all variables, we can do this:
此外,如果我们需要访问所有变量,也可以这样做:
@Test
void givenOS_whenGetEnv_thenVariablesArePresent() {
Map<String, String> environment = System.getenv();
assertThat(environment).isNotNull();
}
However, the System doesn’t expose any setters, and the Map we receive is unmodifiable.
但是,System 并未公开任何设置器,而且我们收到的 Map 是 不可修改的。
3. Changing Environment Variables
3.更改环境变量
We can have different cases where we want to change or set an environment variable. As our processes are involved in a hierarchy, thus we have three options:
我们可以在不同情况下更改或设置环境变量。由于我们的进程涉及一个层次结构,因此我们有三种选择:
- a child process changes/sets the environment variable of a parent
- a process changes/sets its environment variables
- a parent process changes/sets the environment variables of a child
We’ll talk only about the last two cases. The first one is more complex and cannot be easily rationalized for test purposes. Also, it generally cannot be achieved in pure Java and often involves some advanced coding in C/C++.
我们只讨论后两种情况。第一种情况更为复杂,不容易为测试目的而合理化。而且,这通常无法用纯 Java 实现,往往需要用 C/C++ 进行一些高级编码。
We’ll concentrate only on Java solutions to this problem. Although JNI is part of Java, it’s more involved, and the solution should be implemented in C/C++. Also, the solution might have issues with portability. That’s why we won’t investigate these approaches in detail.
虽然 JNI 是 Java 的一部分,但它涉及的内容较多,因此解决方案应在 C/C++ 中实现。此外,该解决方案还可能存在可移植性问题。这就是我们不详细研究这些方法的原因。
4. Current Process
4.当前流程
Here, we have several options. Some of them might be viewed as hacks, as it’s not guaranteed that they will work on all the platforms.
在这里,我们有几种选择。其中一些可能会被视为黑客行为,因为不能保证它们能在所有平台上运行。
4.1. Using Reflection API
4.1.使用反射 API
Technically, we can change the System class to ensure that it will provide us with the values we need using Reflection API:
从技术上讲,我们可以更改 System 类,以确保它将使用 Reflection API 为我们提供所需的值:
@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;
}
However, this approach would break the boundaries of modules. Thus, on Java 9 and above, it might result in a warning, but the code will compile. While in Java 16 and above, it throws an error:
然而,这种方法将打破模块的界限。因此,在 Java 9 及以上版本中,它可能会导致警告,但代码仍可编译。而在 Java 16 及以上版本中,则会出现错误:
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
To overcome the latter problem, we need to open the system modules for reflective access. We can use the following VM options:
要解决后一个问题,我们需要打开系统模块,以便进行反射访问。我们可以使用以下虚拟机选项:
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
While running this code from a module, we can use its name instead of ALL-UNNAMED.
在模块中运行这段代码时,我们可以使用其名称,而不是 ALL-UNNAMED 。
However, the getenv(String) implementation might differ from platform to platform. Also, we don’t have any guarantees about the API of internal classes, so the solution might not work in all setups.
但是,getenv(String) 的实现可能因平台而异。此外,我们无法保证内部类的 API,因此该解决方案可能无法在所有设置中运行。
To save some typing, we can use an already implemented solution from the JUnit Pioneer library:
为了节省输入,我们可以使用 JUnit Pioneer 库中已经实现的解决方案:
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
It uses a similar idea but offers a more declarative approach:
它采用了类似的理念,但提供了一种更具声明性的方法:
@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);
}
@SetEnvironmentVariable helps us to define the environment variables. However, because it uses reflection, we have to provide access to the closed modules as we did previously.
@SetEnvironmentVariable 可以帮助我们定义环境变量。不过,由于它使用了反射,我们必须像之前那样提供对封闭模块的访问。
4.2. JNI
4.2 JNI
Another approach is to use JNI and implement the code that would set the environment variables using C/C++. It’s a more invasive approach and requires minimal C/C++ skills. At the same time, it doesn’t have a problem with reflexive access.
另一种方法是使用 JNI 并使用 C/C++ 实现设置环境变量的代码。这是一种更具侵入性的方法,只需最低限度的 C/C++ 技能。同时,它不存在反射访问的问题。
However, we cannot guarantee that it will update the variables in Java runtime. Our application can cache the variables on startup, and any further changes won’t have any effect. We don’t have this problem while changing the underlying Map using reflection, as it changes the value only on the Java side.
但是,我们不能保证它会在 Java 运行时更新变量。我们的应用程序可以在启动时缓存变量,任何进一步的更改都不会产生任何影响。在使用反射更改底层 Map 时,我们不会遇到这个问题,因为它只在 Java 端更改值。
Also, this approach would require a custom solution for different platforms. Because all OSs handle environment variables differently, the solution won’t be as cross-platform as the pure Java implementation.
此外,这种方法还需要为不同平台定制解决方案。由于所有操作系统处理环境变量的方式不同,因此该解决方案不会像纯 Java 实现那样具有跨平台性。
5. Child Process
5.儿童进程
ProcessBuilder can help us to create a child process directly from Java. It’s possible to run any process with it. However, we’ll use it to run our JUnit tests:
ProcessBuilder 可以帮助我们直接从 Java 创建子进程。使用它可以运行任何进程。不过,我们将用它来运行 JUnit 测试:
@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_VARIABLE_VALUE);
Process process = processBuilder.command(arguments).start();
int errorCode = process.waitFor();
assertThat(errorCode).isZero();
}
ProcessBuilder provides API to access environment variables and start a separate process. We can even run a Maven test goal and identify which tests we want to execute:
ProcessBuilder 提供了访问环境变量和启动单独进程的 API。我们甚至可以运行 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};
This process picks up the tests in the same class with a specific tag:
这个过程会拾取同一类别中带有特定标签的测试:
@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_VARIABLE_VALUE);
}
It’s possible to customize this solution and tailor it to specific requirements.
该解决方案可根据具体要求进行定制。
6. Docker Environment
6.Docker 环境
However, if we need more configuration or a more specific environment, it’s better to use Docker and Testcontainers. It would provide us with more control, especially with integration tests. Let’s outline the Dockerfile first:
但是,如果我们需要更多配置或更特殊的环境,最好使用 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
We’ll copy the required test and run it inside a container. Also, we provide environment variables in the same file.
我们将复制所需的测试并在容器中运行。此外,我们还将在同一文件中提供环境变量。
We can use a CI/CD setup to pick up the container or Testcontainers inside our tests to run the test. While it’s not the most elegant solution, it might help us run all the tests in a single click. Let’s consider a simplistic example:
我们可以使用 CI/CD 设置拾取容器或测试中的 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);
}
}
However, containers don’t provide a convenient API to copy a folder to get all reports. The simplest way to do this is the withFileSystemBind() method, but it’s deprecated. Another approach is to create a bind in the Dockerfile directly.
但是,容器并没有提供方便的 API 来复制文件夹以获取所有报告。最简单的方法是使用 withFileSystemBind() 方法,但该方法已被弃用。另一种方法是直接在 Dockerfile 中创建绑定。
We can rewrite the example using ProcessBuillder. The main idea is to tie the Docker and usual tests into the same suite.
我们可以使用 ProcessBuillder 重写示例。主要思路是将 Docker 测试和常规测试绑定到同一个套件中。
7. Conclusion
7.结论
Java allows us to work with the environment variables directly. However, changing their values or setting new ones isn’t easy.
Java 允许我们直接使用环境变量。不过,更改变量值或设置新变量并不容易。
If we need this in our domain logic, it signals that we’ve violated several SOLID principles in most cases. However, during testing, more control over environment variables might simplify the process and allow us to check more specific cases.
如果我们在领域逻辑中需要这样做,那么在大多数情况下,这意味着我们已经违反了若干SOLID原则。不过,在测试过程中,对环境变量的更多控制可能会简化过程,并允许我们检查更多特定情况。
Although we can use reflection, spinning a new process or building an entirely new environment using Docker is a more appropriate solution.
虽然我们可以使用反射,但使用 Docker 旋转一个新进程或构建一个全新的环境是更合适的解决方案。
As usual, all the code from this tutorial is available over on GitHub.
与往常一样,本教程中的所有代码都可以在 GitHub 上获取。