在 Java 運行時設定環境變數
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. 更改環境變數
我們可能會在不同的情況下想要更改或設定環境變數。由於我們的流程涉及層次結構,因此我們有三個選擇:
- 子進程更改/設定父進程的環境變數
- 進程更改/設定其環境變數
- 父進程更改/設定子進程的環境變數
我們只討論最後兩種情況。第一個比較複雜,不能輕易地出於測試目的而合理化。而且,它通常無法用純 Java 實現,並且通常涉及一些 C/C++ 中的高階編碼。
我們將只關注這個問題的 Java 解決方案。 JNI雖然是Java的一部分,但是涉及的比較多,解決方案應該用C/C++來實作。此外,該解決方案可能存在可移植性問題。這就是為什麼我們不會詳細研究這些方法。
4. 目前流程
在這裡,我們有幾種選擇。其中一些可能被視為駭客,因為不能保證它們適用於所有平台。
4.1.使用反射 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;
}
然而,這種方法會打破模組的界限。因此,在 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
為了克服後一個問題,我們需要打開系統模組進行反射存取。我們可以使用以下虛擬機器選項:
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
從模組執行此程式碼時,我們可以使用其名稱而不是 ALL-UNNAMED。
但是, getenv(String)
實作可能因平台而異。此外,我們對內部類別的 API 沒有任何保證,因此該解決方案可能不適用於所有設定。
為了節省一些輸入,我們可以使用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);
}
@SetEnvironmentVariable
幫助我們定義環境變數。但是,由於它使用反射,我們必須像以前一樣提供對封閉模組的存取。
4.2. JNI
另一種方法是使用 JNI 並使用 C/C++ 實作設定環境變數的程式碼。這是一種更具侵入性的方法,並且需要最少的 C/C++ 技能。同時,它也不存在反射存取的問題。
但是,我們不能保證它會更新Java執行時間中的變數。我們的應用程式可以在啟動時快取變量,任何進一步的更改都不會產生任何影響。使用反射變更底層Map
時不會遇到此問題,因為它只會變更 Java 端的值。
此外,這種方法需要針對不同平台的客製化解決方案。由於所有作業系統處理環境變數的方式都不同,因此該解決方案不會像純 Java 實作那樣跨平台。
5. 子進程
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
提供 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};
此程序使用特定標籤選取同一類別中的測試:
@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);
}
可以自訂該解決方案並根據特定要求進行客製化。
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
我們將複製所需的測試並在容器內運行它。此外,我們在同一文件中提供環境變數。
我們可以使用 CI/CD 設定來選取測試中的容器或測試容器來執行測試。雖然它不是最優雅的解決方案,但它可能幫助我們只需單擊即可運行所有測試。讓我們考慮一個簡單的例子:
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);
}
}
但是,容器不提供方便的 API 來複製資料夾以取得所有報告。最簡單的方法是使用withFileSystemBind()
方法,但它已被棄用。另一種方法是直接在 Dockerfile 中建立綁定。
我們可以使用ProcessBuillder
重寫該範例。主要想法是將 Docker 和常用測試綁定到同一個套件中。
七、結論
Java 允許我們直接使用環境變數。然而,改變他們的價值觀或設定新的價值觀並不容易。
如果我們在領域邏輯中需要這個,則表示我們在大多數情況下違反了幾項 SOLID 原則。然而,在測試過程中,對環境變數的更多控制可能會簡化過程並允許我們檢查更具體的情況。
雖然我們可以使用反射,但使用 Docker 旋轉一個新進程或建立一個全新的環境是更合適的解決方案。
與往常一樣,本教程中的所有程式碼都可以在 GitHub 上取得。