Have you ever needed to access or modify environment variables in your Java application? If you’re like me, you’ve probably reached for System.getenv() to read variables, but then scratched your head wondering how to update them.
In today’s post, I’ll dive into environment variables in Java 21 - something I’ve been playing with recently. We’ll look at how to read them (the easy part) and explore different approaches to modify them (the not-so-easy part).
TLDR#
Java makes reading env variables easy (System.getenv()), but modifying them is restricted
Four options for modification:#
- ProcessBuilder for child processes only
- JNA for native system function access
- Reflection hacks (avoid in production!)
- Native OS commands (setx/export)
- Best practice: Load env variables at startup rather than modifying at runtime
What are Environment Variables anyway?#
Before jumping into code, let’s take a step back. Environment variables are basically key-value pairs that live outside your application. They’re part of the environment in which your process runs.
I like to think of them as a global configuration that follows your application around. They’re incredibly useful for things like:
- Storing database connection strings (so they’re not hardcoded in your app)
- Setting up API keys (without exposing them in your code repository)
- Configuring different behavior across environments (dev/staging/prod)
- Storing system paths and user information
If you’re working with containerized applications or microservices, you’re probably already using environment variables extensively.
Reading Environment Variables in Java#
Let’s start with the easy part - reading environment variables. Java provides a couple of straightforward methods for this.
The Classic System.getenv() Approach#
This is the method I use :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import java.util.Map;
public class EnvReader {
public static void main(String[] args) {
// Get a specific variable
String path = System.getenv("PATH");
System.out.println("My PATH is: " + path);
// Or get all environment variables at once
Map<String, String> env = System.getenv();
System.out.println("All my environment variables:");
env.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
|
Simple and effective! You can either get a specific variable by name or grab all of them as a Map.
Using ProcessBuilder for Environment Access
There’s another approach using ProcessBuilder that I find useful, especially when working with subprocesses:
1
2
3
4
5
6
7
8
9
10
11
| import java.util.Map;
public class ProcessBuilderEnvReader {
public static void main(String[] args) {
ProcessBuilder processBuilder = new ProcessBuilder();
Map<String, String> environment = processBuilder.environment();
System.out.println("My environment variables via ProcessBuilder:");
environment.forEach((key, value) -> System.out.println(key + " = " + value));
}
}
|
The nice thing about ProcessBuilder is that you can modify this environment map for any subprocesses you launch - more on that shortly.
The Tricky Part: Modifying Environment Variables#
Now comes the challenging part. Unlike reading environment variables, Java doesn’t provide direct methods to modify them in the current process. This is actually by design for security reasons.
However, there are several workarounds we can use. Let’s explore them.
Approach 1: Modifying Environment for Child Processes#
This is the most straightforward approach. While you can’t change the current process’s environment, you can set environment variables for any child processes you launch:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import java.io.IOException;
public class EnvSetterForChildProcess {
public static void main(String[] args) {
try {
ProcessBuilder pb = new ProcessBuilder("java", "-version");
// Here's where the magic happens
pb.environment().put("MY_CUSTOM_VAR", "Hello from parent!");
Process process = pb.start();
int exitCode = process.waitFor();
System.out.println("Process finished with exit code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
|
The limitation? This only affects the child process, not your current JVM or the system as a whole.
Approach 2: Using JNA (Java Native Access)#
For more control, sometimes we should look into JNA, which lets you access native system functions:
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
| import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
public class JNAEnvSetter {
// Interface to C library functions
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CLibrary.class);
int setenv(String name, String value, int overwrite);
int unsetenv(String name);
}
public static void main(String[] args) {
// Set an environment variable
int result = CLibrary.INSTANCE.setenv("MY_ENV_VAR", "my_value", 1);
if (result == 0) {
System.out.println("Environment variable set successfully");
// Verify by reading it back
String value = System.getenv("MY_ENV_VAR");
System.out.println("MY_ENV_VAR: " + value);
} else {
System.out.println("Failed to set environment variable");
}
}
}
|
Remember to add JNA to your project dependencies if you want to try this approach.
Approach 3: The Reflection Hack (Use with Caution!)#
I’ve seen this approach used, but I generally don’t recommend it for production code. It uses reflection to modify the private environment map:
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
| import java.lang.reflect.*;
import java.util.Map;
public class ReflectionEnvSetter {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
// Attempt to update environment variables via ProcessEnvironment fields.
Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
Field envField = processEnvironmentClass.getDeclaredField("theEnvironment");
envField.setAccessible(true);
Map<String, String> env = (Map<String, String>) envField.get(null);
env.putAll(newEnv);
// Try to update the case-insensitive environment as well, if present.
try {
Field ciEnvField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
ciEnvField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, String> ciEnv = (Map<String, String>) ciEnvField.get(null);
ciEnv.putAll(newEnv);
} catch (NoSuchFieldException nsfe) {
// Field not present on this system (e.g., non-Windows), ignore.
}
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
// Fallback: Modify the unmodifiable map returned by System.getenv()
try {
Map<String, String> sysEnv = System.getenv();
// Use reflection to access the internal modifiable map
Field field = sysEnv.getClass().getDeclaredField("m");
field.setAccessible(true);
Map<String, String> modifiableEnv = (Map<String, String>) field.get(sysEnv);
modifiableEnv.putAll(newEnv);
} catch (Exception ex) {
throw new RuntimeException("Failed to set environment variables", ex);
}
}
}
}
|
Why don’t I recommend this? It’s:
- Not guaranteed to work across JVM implementations
- May behave differently in future Java versions
- May lead to unexpected behavior or security issues
I’ve seen it not work properly so proceed with caution!
Approach 4: Native OS Commands#
Sometimes the simplest approach is to just execute the appropriate OS command:
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
| import java.io.IOException;
public class NativeEnvSetter {
public static void main(String[] args) {
try {
ProcessBuilder processBuilder;
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
// Windows way
processBuilder = new ProcessBuilder("cmd", "/c", "setx", "MY_ENV_VAR", "my_value");
} else {
// Unix/Linux/Mac way
processBuilder = new ProcessBuilder("/bin/sh", "-c", "export MY_ENV_VAR=my_value");
}
Process process = processBuilder.start();
int exitCode = process.waitFor();
System.out.println("Command executed with exit code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
|
On Windows, setx creates a persistent environment variable. On Unix-like systems, export sets it for the current shell session, but it won’t persist across sessions unless you add it to your profile files.
Removing Environment Variables#
Removing environment variables follows similar patterns to creating/updating them:
For Child Processes#
1
2
| ProcessBuilder pb = new ProcessBuilder("java", "-version");
pb.environment().remove("MY_CUSTOM_VAR");
|
Using JNA#
1
| int result = JNAEnvSetter.CLibrary.INSTANCE.unsetenv("MY_ENV_VAR");
|
Using OS Commands#
1
2
3
4
5
6
7
8
9
10
11
| // For Windows
ProcessBuilder pb = new ProcessBuilder(
"cmd", "/c",
"REG DELETE \"HKCU\\Environment\" /V MY_ENV_VAR /f"
);
// For Unix/Linux/Mac
ProcessBuilder pb = new ProcessBuilder(
"/bin/sh", "-c",
"unset MY_ENV_VAR"
);
|
Putting It All Together: A Practical Environment Manager#
Let me wrap this up with a complete environment variable manager class that you might find useful in your projects:
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
| import java.io.IOException;
import java.util.Map;
import java.util.Optional;
public class EnvironmentManager {
/**
* Read a specific environment variable
*/
public static Optional<String> read(String name) {
return Optional.ofNullable(System.getenv(name));
}
/**
* Read all environment variables
*/
public static Map<String, String> readAll() {
return System.getenv();
}
/**
* Set an environment variable for child processes
*/
public static boolean setForChildProcesses(String name, String value) {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.environment().put(name, value);
return true;
}
/**
* Set a system-wide environment variable
*/
public static boolean setSystemwide(String name, String value) {
try {
ProcessBuilder processBuilder;
if (isWindows()) {
processBuilder = new ProcessBuilder("cmd", "/c", "setx", name, value);
} else {
String command = String.format("export %s=\"%s\" && echo \"export %s=%s\" >> ~/.profile",
name, value, name, value);
processBuilder = new ProcessBuilder("/bin/sh", "-c", command);
}
Process process = processBuilder.start();
int exitCode = process.waitFor();
return exitCode == 0;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return false;
}
}
/**
* Delete an environment variable system-wide
*/
public static boolean delete(String name) {
try {
ProcessBuilder processBuilder;
if (isWindows()) {
processBuilder = new ProcessBuilder(
"cmd", "/c",
"REG DELETE \"HKCU\\Environment\" /V " + name + " /f"
);
} else {
String command = String.format("unset %s && sed -i '/export %s=/d' ~/.profile", name, name);
processBuilder = new ProcessBuilder("/bin/sh", "-c", command);
}
Process process = processBuilder.start();
int exitCode = process.waitFor();
return exitCode == 0;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return false;
}
}
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
}
|