Domanda

Come posso impostare le variabili di ambiente da Java? Vedo che posso farlo per i sottoprocessi usando ProcessBuilder . Tuttavia, ho diversi sottoprocessi da avviare, quindi preferirei modificare l'ambiente del processo corrente e lasciare che i sottoprocessi lo ereditino.

C'è un System.getenv(String) per ottenere una singola variabile d'ambiente. Posso anche ottenere un Map del set completo di variabili d'ambiente con System.getenv(). Ma chiamare put() su quel UnsupportedOperationException genera un System.setenv() - apparentemente significano che l'ambiente è di sola lettura. E non c'è <=>.

Quindi, c'è un modo per impostare le variabili di ambiente nel processo attualmente in esecuzione? Se é cosi, come? In caso contrario, qual è la logica? (È perché si tratta di Java e quindi non dovrei fare cose obsolete non portabili malvagie come toccare il mio ambiente?) E in caso contrario, qualche buon suggerimento per la gestione dei cambiamenti delle variabili di ambiente che dovrò alimentare con diversi sottoprocessi?

È stato utile?

Soluzione

  

(È perché questo è Java e quindi non dovrei fare cose obsolete non portabili malvagie come toccare il mio ambiente?)

Penso che tu abbia colpito l'unghia sulla testa.

Un modo possibile per alleggerire l'onere sarebbe quello di scomporre un metodo

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

e passaci sopra ProcessBuilder s prima di avviarli.

Probabilmente lo sai già, ma puoi avviare più di un processo con lo stesso <=>. Quindi, se i tuoi sottoprocessi sono gli stessi, non è necessario ripetere questa configurazione più volte.

Altri suggerimenti

Per l'utilizzo in scenari in cui è necessario impostare valori di ambiente specifici per unit test, è possibile che sia utile il seguente hack. Cambierà le variabili di ambiente in tutta la JVM (quindi assicurati di reimpostare eventuali modifiche dopo il test), ma non altererà l'ambiente di sistema.

Ho scoperto che una combinazione dei due hack sporchi di Edward Campbell e anonimo funziona meglio, poiché uno dei non funziona sotto Linux, uno non funziona sotto Windows 7. Quindi per ottenere un hack malevolo multipiattaforma li ho combinati:

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

Funziona come un fascino. Crediti completi per i due autori di questi hack.

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

O per aggiungere / aggiornare un singolo var e rimuovere il loop secondo il suggerimento di thejoshwolfe.

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

su Android l'interfaccia è esposta tramite Libcore.os come una sorta di API nascosta.

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

La classe Libcore e il sistema operativo dell'interfaccia sono pubblici. Manca solo la dichiarazione di classe e deve essere mostrata al linker. Non è necessario aggiungere le classi all'applicazione, ma non fa male se è incluso.

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

Solo Linux

Impostazione di variabili d'ambiente singole (basate sulla risposta di 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);
    }
}

Utilizzo:

Innanzitutto, inserisci il metodo in qualsiasi classe desideri, ad es. SystemUtil.

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

Se chiami System.getenv("SHELL") dopo questo, otterrai "/bin/bash" indietro.

Si scopre che la soluzione di @ pushy / @ anonymous / @ Edward Campbell non funziona su Android perché Android non è proprio Java. In particolare, Android non ha java.lang.ProcessEnvironment affatto. Ma risulta essere più facile in Android, devi solo fare una chiamata JNI a 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;
}

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

Questa è una combinazione della risposta di @ paul-blair convertita in Java che include alcune pulizie evidenziate da Paul Blair e alcuni errori che sembrano essere stati nel codice di @pushy che è composto da @Edward Campbell e anonimo.

Non posso sottolineare quanto questo codice debba essere utilizzato SOLO nei test ed è estremamente confuso. Ma per i casi in cui è necessaria l'installazione dell'ambiente nei test, è esattamente quello di cui avevo bisogno.

Ciò include anche alcuni miei tocchi minori che consentono al codice di funzionare su entrambi i Windows in esecuzione su

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)

e Centos in esecuzione

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)

L'implementazione:

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

Cercando online, sembra che sia possibile farlo con JNI. Dovresti quindi effettuare una chiamata a putenv () da C e (presumibilmente) dovresti farlo in un modo che funzionasse sia su Windows che su UNIX.

Se tutto ciò che può essere fatto, sicuramente non sarebbe troppo difficile per Java stesso supportare questo invece di mettermi in una giacca dritta.

Un amico che parla Perl altrove suggerisce che ciò è dovuto al fatto che le variabili di ambiente sono globali di processo e Java sta cercando un buon isolamento per una buona progettazione.

Ho provato la risposta di Pushy sopra e ha funzionato per la maggior parte. Tuttavia, in determinate circostanze, vedrei questa eccezione:

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

Ciò si verifica quando il metodo è stato chiamato più di una volta, a causa dell'implementazione di alcune classi interne di ProcessEnvironment. Se il metodo setEnv(..) viene chiamato più di una volta, quando le chiavi vengono recuperate da <= > map, ora sono stringhe (essendo state inserite come stringhe dalla prima invocazione di theEnvironment) e non possono essere trasmesse al tipo generico della mappa, setEnv(...) che è una classe interna privata di Variable,

Una versione fissa (in Scala) è sotto. Spero che non sia troppo difficile trasferirsi in 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()
  }
}

Come la maggior parte delle persone che hanno trovato questo thread, stavo scrivendo alcuni test unitari e avevo bisogno di modificare le variabili di ambiente per impostare le condizioni corrette per l'esecuzione del test. Tuttavia, ho scoperto che le risposte più votate avevano alcuni problemi e / o erano molto criptiche o eccessivamente complicate. Speriamo che questo possa aiutare gli altri a risolvere la soluzione più rapidamente.

Prima di tutto, ho finalmente trovato la soluzione di @Hubert Grzeskowiak per essere la più semplice e ha funzionato per me. Vorrei essere venuto prima a quello. Si basa sulla risposta di @Edward Campbell, ma senza complicare la ricerca in loop.

Tuttavia, ho iniziato con la soluzione di @ pushy, che ha ottenuto il maggior numero di voti. È una combinazione di @anonymous e @Edward Campbell. @pushy afferma che entrambi gli approcci sono necessari per coprire ambienti Linux e Windows. Sono in esecuzione su OS X e trovo che entrambi funzionano (una volta risolto un problema con l'approccio @anonymous). Come altri hanno notato, questa soluzione funziona la maggior parte delle volte, ma non tutti.

Penso che la fonte della maggior parte della confusione derivi dalla soluzione di @ anonymous operante nel campo 'theEnvironment'. Guardando la definizione di ProcessEnvironment ," theEnvironment "non è una mappa < String, String & Gt; ma piuttosto è una mappa < Variabile, Valore & Gt ;. L'eliminazione della mappa funziona correttamente, ma l'operazione putAll ricostruisce la mappa in una mappa & Lt; String, String & Gt ;, che potenzialmente causa problemi quando le operazioni successive operano sulla struttura dei dati utilizzando l'API normale che prevede Map & Lt; Variabile, Valore & Gt ;. Inoltre, l'accesso / rimozione di singoli elementi è un problema. La soluzione è accedere indirettamente all '"Ambiente" tramite "Ambiente immodificabile". Ma poiché questo è un tipo UnmodifiableMap l'accesso deve essere effettuato tramite la variabile privata 'm' del tipo UnmodifiableMap. Vedi getModifiableEnvironmentMap2 nel codice seguente.

Nel mio caso avevo bisogno di rimuovere alcune delle variabili d'ambiente per il mio test (le altre dovrebbero essere invariate). Quindi ho voluto ripristinare le variabili di ambiente al loro stato precedente dopo il test. Le routine sottostanti lo rendono semplice. Ho testato entrambe le versioni di getModifiableEnvironmentMap su OS X ed entrambe funzionano in modo equivalente. Sebbene basato sui commenti in questo thread, uno potrebbe essere una scelta migliore dell'altro a seconda dell'ambiente.

Nota: non ho incluso l'accesso a "theCaseInsensitiveEnvironmentField" poiché sembra essere specifico di Windows e non ho avuto modo di testarlo, ma aggiungerlo dovrebbe essere semplice.

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

Questa è la versione malvagia di Kotlin del male di @ pushy 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)
            }
        }
    }

Funziona almeno in macOS Mojave.

Implementazione di Kotlin che ho recentemente fatto sulla base della risposta di Edward:

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

Puoi passare i parametri nel tuo processo java iniziale con -D:

java -cp <classpath> -Dkey1=value -Dkey2=value ...
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top