336
votes

How do I set environment variables from Java? I see that I can do this for subprocesses using ProcessBuilder. I have several subprocesses to start, though, so I'd rather modify the current process's environment and let the subprocesses inherit it.

There's a System.getenv(String) for getting a single environment variable. I can also get a Map of the complete set of environment variables with System.getenv(). But, calling put() on that Map throws an UnsupportedOperationException -- apparently they mean for the environment to be read only. And, there's no System.setenv().

So, is there any way to set environment variables in the currently running process? If so, how? If not, what's the rationale? (Is it because this is Java and therefore I shouldn't be doing evil nonportable obsolete things like touching my environment?) And if not, any good suggestions for managing the environment variable changes that I'm going to need to be feeding to several subprocesses?

20
System.getEnv() is intended to be universal-ish, some environments don't even have environment variables.b1nary.atr0phy
For anyone who needed this for a unit testing use case: stackoverflow.com/questions/8168884/…Atifm

20 Answers

99
votes

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

I think you've hit the nail on the head.

A possible way to ease the burden would be to factor out a method

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

and pass any ProcessBuilders through it before starting them.

Also, you probably already know this, but you can start more than one process with the same ProcessBuilder. So if your subprocesses are the same, you don't need to do this setup over and over.

248
votes

For use in scenarios where you need to set specific environment values for unit tests, you might find the following hack useful. It will change the environment variables throughout the JVM (so make sure you reset any changes after your test), but will not alter your system environment.

I found that a combination of the two dirty hacks by Edward Campbell and anonymous works best, as one of the does not work under linux, one does not work under windows 7. So to get a multiplatform evil hack I combined them:

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);
      }
    }
  }
}

This Works like a charm. Full credits to the two authors of these hacks.

77
votes
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);
        }
    }
}

Or to add/update a single var and removing the loop as per thejoshwolfe's suggestion.

@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);
  }
23
votes

Linux/MacOS only

Setting single environment variables (based on answer by Edward Campbell):

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);
    }
}

Usage:

First, put the method in any class you want, e.g. SystemUtil. Then call it statically:

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

If you call System.getenv("SHELL") after this, you'll get "/bin/bash" back.

22
votes
// 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);
}
18
votes

on Android the interface is exposed via Libcore.os as a kind of hidden API.

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

The Libcore class as well as the interface OS is public. Just the class declaration is missing and need to be shown to the linker. No need to add the classes to the application, but it also does not hurt if it is included.

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;
}
10
votes

This is a combination of @paul-blair 's answer converted to Java which includes some cleanups pointed out by paul blair and some mistakes that seem to have been inside @pushy 's code which is made up of @Edward Campbell and anonymous.

I cannot emphasize how much this code should ONLY be used in testing and is extremely hacky. But for cases where you need the environment setup in tests it is exactly what I needed.

This also includes some minor touches of mine that allow the code to work on both Windows running on

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)

as well as Centos running on

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)

The implementation:

/**
 * 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));
}
8
votes

It turns out that the solution from @pushy/@anonymous/@Edward Campbell does not work on Android because Android is not really Java. Specifically, Android does not have java.lang.ProcessEnvironment at all. But it turns out to be easier in Android, you just need to do a JNI call to POSIX setenv():

In C/JNI:

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;
}

And in Java:

public class Posix {

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

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

Like most people who have found this thread, I was writing some unit tests and needed to modify the environment variables to set the correct conditions for the test to run. However, I found the most upvoted answers had some issues and/or were very cryptic or overly complicated. Hopefully this will help others to sort out the solution more quickly.

First off, I finally found @Hubert Grzeskowiak's solution to be the simplest and it worked for me. I wish I would have come to that one first. It's based on @Edward Campbell's answer, but without the complicating for loop search.

However, I started with @pushy's solution, which got the most upvotes. It is a combo of @anonymous and @Edward Campbell's. @pushy claims both approaches are needed to cover both Linux and Windows environments. I'm running under OS X and find that both work (once an issue with @anonymous approach is fixed). As others have noted, this solution works most of the time, but not all.

I think the source of most of the confusion comes from @anonymous's solution operating on the 'theEnvironment' field. Looking at the definition of the ProcessEnvironment structure, 'theEnvironment' is not a Map< String, String > but rather it is a Map< Variable, Value >. Clearing the map works fine, but the putAll operation rebuilds the map a Map< String, String >, which potentially causes problems when subsequent operations operate on the data structure using the normal API that expects Map< Variable, Value >. Also, accessing/removing individual elements is a problem. The solution is to access 'theEnvironment' indirectly through 'theUnmodifiableEnvironment'. But since this is a type UnmodifiableMap the access must be done through the private variable 'm' of the UnmodifiableMap type. See getModifiableEnvironmentMap2 in code below.

In my case I needed to remove some of the environment variables for my test (the others should be unchanged). Then I wanted to restore the environment variables to their prior state after the test. The routines below make that straight forward to do. I tested both versions of getModifiableEnvironmentMap on OS X, and both work equivalently. Though based on comments in this thread, one may be a better choice than the other depending on the environment.

Note: I did not include access to the 'theCaseInsensitiveEnvironmentField' since that seems to be Windows specific and I had no way to test it, but adding it should be straight forward.

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);
}
4
votes

Poking around online, it looks like it might be possible to do this with JNI. You'd then have to make a call to putenv() from C, and you'd (presumably) have to do it in a way that worked on both Windows and UNIX.

If all that can be done, it surely wouldn't be too hard for Java itself to support this instead of putting me in a straight jacket.

A Perl-speaking friend elsewhere suggests that this is because environment variables are process global and Java is striving for good isolation for good design.

4
votes

Tried pushy's answer above and it worked for the most part. However, in certain circumstances, I would see this exception:

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

This turns out to happen when the method was called more than once, owing to the implementation of certain inner classes of ProcessEnvironment. If the setEnv(..) method is called more than once, when the keys are retrieved from the theEnvironment map, they are now strings (having been put in as strings by the first invocation of setEnv(...) ) and cannot be cast to the map's generic type, Variable, which is a private inner class of ProcessEnvironment.

A fixed version (in Scala), is below. Hopefully it isn't too difficult to carry over into Java.

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()
  }
}
2
votes

This is the Kotlin evil version of the @pushy's evil answer =)

@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)
            }
        }
    }

It is working in macOS Mojave at least.

1
votes

Tim Ryan's answer worked for me... but I wanted it for Groovy (Spock context for example), and simplissimo:

import java.lang.reflect.Field

def getModifiableEnvironmentMap() {
    def unmodifiableEnv = System.getenv()
    Class cl = unmodifiableEnv.getClass()
    Field field = cl.getDeclaredField("m")
    field.accessible = true
    field.get(unmodifiableEnv)
}

def clearEnvironmentVars( def keys ) {
    def savedVals = [:]
    keys.each{ key ->
        String val = modifiableEnvironmentMap.remove(key)
        // thinking about it, I'm not sure why we need this test for null
        // but haven't yet done any experiments
        if( val != null ) {
            savedVals.put( key, val )
        }
    }
    savedVals
}

def setEnvironmentVars(Map varMap) {
    modifiableEnvironmentMap.putAll(varMap)
}

// pretend existing Env Var doesn't exist
def PATHVal1 = System.env.PATH
println "PATH val1 |$PATHVal1|"
String[] keys = ["PATH", "key2", "key3"]
def savedVars = clearEnvironmentVars(keys)
def PATHVal2 = System.env.PATH
println "PATH val2 |$PATHVal2|"

// return to reality
setEnvironmentVars(savedVars)
def PATHVal3 = System.env.PATH
println "PATH val3 |$PATHVal3|"
println "System.env |$System.env|"

// pretend a non-existent Env Var exists
setEnvironmentVars( [ 'key4' : 'key4Val' ])
println "key4 val |$System.env.key4|"
1
votes

I stumbled upon this thread as I had a similar requirement where I needed to set (or update) an Environment Variable (permanently).

So I looked in to - How to set an environment variable permanently from Command Prompt and it was very simple!

setx JAVA_LOC C:/Java/JDK

Then I just implemented the same in my code Here's what I used (suppose - JAVA_LOC is the env. variable name)

        String cmdCommand = "setx JAVA_LOC " + "C:/Java/JDK";
            ProcessBuilder processBuilder = new ProcessBuilder();
            processBuilder.command("cmd.exe", "/c", cmdCommand);
            processBuilder.start();

ProcessBuilder fires-up a cmd.exe and passes the command that you want. The environment variable is retained even if you kill JVM/reboot the system as it has no relation with the JVM/Program's lifecycle.

0
votes

If you work with SpringBoot you can add specifying the environmental variable in the following property:

was.app.config.properties.toSystemProperties
0
votes

variant based on @pushy's answer, works on windows.

def set_env(newenv):
    from java.lang import Class
    process_environment = Class.forName("java.lang.ProcessEnvironment")
    environment_field =  process_environment.getDeclaredField("theEnvironment")
    environment_field.setAccessible(True)
    env = environment_field.get(None)
    env.putAll(newenv)
    invariant_environment_field = process_environment.getDeclaredField("theCaseInsensitiveEnvironment");
    invariant_environment_field.setAccessible(True)
    invevn = invariant_environment_field.get(None)
    invevn.putAll(newenv)

Usage:

old_environ = dict(os.environ)
old_environ['EPM_ORACLE_HOME'] = r"E:\Oracle\Middleware\EPMSystem11R1"
set_env(old_environ)
0
votes

A version in Kotlin, in this algorithm I created a decorator that allows you to set and get variables from the environment.

import java.util.Collections
import kotlin.reflect.KProperty
​
class EnvironmentDelegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return System.getenv(property.name) ?: "-"
    }
​
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        val key = property.name
​
        val classes: Array<Class<*>> = Collections::class.java.declaredClasses
        val env = System.getenv()
​
        val cl = classes.first { "java.util.Collections\$UnmodifiableMap" == it.name }
​
        val field = cl.getDeclaredField("m")
        field.isAccessible = true
        val obj = field[env]
        val map = obj as MutableMap<String, String>
        map.putAll(mapOf(key to value))
    }
}
​
class KnownProperties {
    var JAVA_HOME: String by EnvironmentDelegate()
    var sample: String by EnvironmentDelegate()
}
​
fun main() {
    val knowProps = KnownProperties()
    knowProps.sample = "2"
​
    println("Java Home: ${knowProps.JAVA_HOME}")
    println("Sample: ${knowProps.sample}")
}
0
votes

There are a three libraries out there that can do this during unit tests.

There's Stefan Birkner's System Rules and System Lambda - https://www.baeldung.com/java-system-rules-junit which allow you to do something like:

public class JUnitTest {

    @Rule
    public EnvironmentVariables environmentVariables = new EnvironmentVariables();

    @Test
    public void someTest() {
        environmentVariables.set("SOME_VARIABLE", "myValue");
        
        // now System.getenv does what you want
    }
}

or for System-Lambda:

@Test
void execute_code_with_environment_variables(
) throws Exception {
  List<String> values = withEnvironmentVariable("first", "first value")
    .and("second", "second value")
    .execute(() -> asList(
      System.getenv("first"),
      System.getenv("second")
    ));
  assertEquals(
    asList("first value", "second value"),
    values
  );
}

The above features are also available as JUnit 5 extension via System Stubs:

@ExtendWith(SystemStubsExtension.class)
class SomeTest {

    @SystemStub
    private EnvironmentVariables;

    @Test
    void theTest() {
        environmentVariables.set("SOME_VARIABLE", "myValue");
        
        // now System.getenv does what you want

    }

}

System Stubs is backwards compatible with System Lambda and System Rules, but supports JUnit 5.

Alternatively, there's also JUnit Pioneer - https://github.com/junit-pioneer/junit-pioneer, which allows for environment variables to be set at test time via annotations.

-1
votes

Kotlin implementation I recently made based on Edward's answer:

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)
    }
}
-12
votes

You can pass parameters into your initial java process with -D:

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