web-dev-qa-db-fra.com

Comment définir des variables d'environnement à partir de Java?

Comment définir des variables d'environnement à partir de Java? Je vois que je peux le faire pour les sous-processus en utilisant ProcessBuilder . J'ai plusieurs sous-processus à démarrer, cependant, je préfère modifier l'environnement du processus actuel et laisser les sous-processus l'hériter.

Il y a System.getenv(String) pour obtenir une seule variable d'environnement. Je peux également obtenir un Map de l'ensemble complet des variables d'environnement avec System.getenv(). Mais appeler put() sur ce Map lance un UnsupportedOperationException - apparemment, cela signifie que l'environnement doit être en lecture seule. Et il n'y a pas de System.setenv().

Alors, est-il possible de définir des variables d'environnement dans le processus en cours d'exécution? Si c'est le cas, comment? Si non, quelle est la raison? (Est-ce parce que c'est Java et que par conséquent je ne devrais pas faire de choses obsolètes non portables, telles que toucher mon environnement?) Et si ce n'est pas le cas, de bonnes suggestions pour la gestion des modifications des variables d'environnement que je vais suivre devez-vous nourrir plusieurs sous-processus?

264
skiphoppy

(Est-ce parce que c'est Java et que par conséquent, je ne devrais pas faire de choses obsolètes non portables comme des démos, comme toucher mon environnement?)

Je pense que vous avez frappé le clou sur la tête.

Un moyen possible d’alléger le fardeau serait de factoriser une méthode

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

et passez tous les ProcessBuilders avant de les démarrer.

En outre, vous le savez probablement déjà, mais vous pouvez démarrer plusieurs processus avec le même ProcessBuilder. Ainsi, si vos sous-processus sont les mêmes, vous n'avez pas besoin de faire cette configuration encore et encore.

84
Michael Myers

Pour une utilisation dans des scénarios dans lesquels vous devez définir des valeurs d’environnement spécifiques pour les tests unitaires, le hack suivant peut être utile. Cela modifiera les variables d'environnement tout au long de la machine virtuelle (assurez-vous donc de réinitialiser toutes les modifications après votre test), mais ne modifiera pas votre environnement système.

J'ai trouvé qu'une combinaison des deux hacks sales d'Edward Campbell et anonymous fonctionne le mieux, car l'un des ne fonctionne pas sous Linux, on ne fonctionne pas sous Windows 7. Donc, pour obtenir un hack multiplateforme mal, je les ai combinés:

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

Cela fonctionne comme un charme. Crédits complets aux deux auteurs de ces hacks.

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

Ou ajouter/mettre à jour une seule variable et supprimer la boucle selon la suggestion de 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);
  }
53
Edward Campbell
// 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);
}
17
anonymous

sur Android, l'interface est exposée via Libcore.os comme une sorte d'API cachée.

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

La classe Libcore ainsi que l'interface OS sont publiques. Seule la déclaration de classe est manquante et doit être affichée à l'éditeur de liens. Pas besoin d'ajouter les classes à l'application, mais cela ne fait pas de mal s'il est inclus.

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

Linux uniquement

Définition de variables d’environnement uniques (basées sur la réponse d’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);
    }
}

tilisation:

Commencez par placer la méthode dans la classe de votre choix, par exemple. SystemUtil.

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

Si vous appelez System.getenv("Shell") après cela, vous récupérez "/bin/bash".

11
Hubert Grzeskowiak

Il s'avère que la solution de @ pushy/@ anonymous/@ Edward Campbell ne fonctionne pas sous Android car Android n'est pas vraiment Java. Plus précisément, Android n'a pas du tout Java.lang.ProcessEnvironment. Mais cela s’avère plus simple sous Android, il suffit de faire un appel JNI à POSIX setenv():

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

Et en 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

Ceci est une combinaison de la réponse de @ paul-blair convertie en Java, qui inclut certains nettoyages signalés par paul blair et des erreurs qui semblent avoir été contenues dans le code de @pushy qui est composé de @ Edward Campbell et anonyme.

Je ne peux pas souligner à quel point ce code devrait SEULEMENT être utilisé dans les tests et est extrêmement bidirectionnel. Mais dans les cas où vous avez besoin de la configuration de l'environnement dans les tests, c'est exactement ce dont j'avais besoin.

Cela inclut également quelques petites manipulations permettant au code de fonctionner à la fois sous Windows et

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)

ainsi que Centos fonctionnant sur

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)

La mise en oeuvre:

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

Comme la plupart des gens qui ont trouvé ce fil de discussion, j’écrivais des tests unitaires et devais modifier les variables d’environnement pour définir les conditions correctes permettant l’exécution du test. Cependant, j’ai trouvé que les réponses les plus votées posaient quelques problèmes et/ou étaient très cryptiques ou trop compliquées. Espérons que cela aidera les autres à résoudre la solution plus rapidement.

Tout d'abord, j'ai finalement trouvé la solution de @Hubert Grzeskowiak la plus simple et elle a fonctionné pour moi. J'aurais aimé venir à celui-là en premier. Il est basé sur la réponse de Edward Campbell, mais sans compliquer la recherche de boucle.

Cependant, j'ai commencé avec la solution de @ pushy, qui a reçu le plus de votes positifs. C'est un combo de @ anonyme et @ Edward Campbell. @pushy affirme que les deux approches sont nécessaires pour couvrir les environnements Linux et Windows. J'exécute sous OS X et constate que les deux fonctionnent (une fois qu'un problème avec @anonymous est résolu). Comme d'autres l'ont noté, cette solution fonctionne la plupart du temps, mais pas dans sa totalité.

Je pense que la confusion provient en grande partie de la solution de @ anonymous opérant sur le champ "theEnvironment". En regardant la définition de la structure ProcessEnvironment , 'theEnvironment' n'est pas une carte <String, String> mais plutôt une carte <Variable, Valeur>. Effacer la carte fonctionne bien, mais l'opération putAll reconstruit la carte en tant que mappage <String, String>, ce qui peut entraîner des problèmes lorsque des opérations ultérieures fonctionnent sur la structure de données à l'aide de l'API normale qui attend Map <Variable, Valeur>. En outre, l'accès/la suppression d'éléments individuels est un problème. La solution consiste à accéder "à l'environnement" indirectement par le biais de "l'environnement non modifiable". Mais puisqu'il s'agit d'un type nmodifiableMap , l'accès doit être effectué via la variable privée 'm' du type UnmodifiableMap. Voir getModifiableEnvironmentMap2 dans le code ci-dessous.

Dans mon cas, je devais supprimer certaines des variables d'environnement de mon test (les autres ne devraient pas être modifiées). Ensuite, je voulais restaurer les variables d’environnement à leur état antérieur après le test. Les routines ci-dessous facilitent grandement cette tâche. J'ai testé les deux versions de getModifiableEnvironmentMap sur OS X, et les deux fonctionnent de manière équivalente. Bien que basé sur les commentaires de ce fil, l'un peut être un meilleur choix que l'autre en fonction de l'environnement.

Remarque: je n'ai pas inclus l'accès à 'theCaseInsensitiveEnvironmentField' car il semble être spécifique à Windows et je n'avais aucun moyen de le tester, mais son ajout devrait être simple.

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);
}
3
Tim Ryan

En fouillant en ligne, il semble possible de le faire avec JNI. Vous devrez alors faire un appel à putenv () depuis C et vous devrez (probablement) le faire de manière à fonctionner à la fois sous Windows et sous UNIX.

Si tout cela pouvait être fait, il ne serait sûrement pas trop difficile pour Java de le supporter au lieu de me mettre dans une veste droite.

Un ami Perl parlant ailleurs, suggère que cela est dû au fait que les variables d'environnement sont des processus globaux et que Java s'efforce d'obtenir une bonne isolation pour une bonne conception.

3
skiphoppy

J'ai essayé la réponse de Pushy ci-dessus et cela a fonctionné pour la plupart. Cependant, dans certaines circonstances, je verrais cette exception:

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

Cela se produit lorsque la méthode a été appelée plus d'une fois, en raison de l'implémentation de certaines classes internes de ProcessEnvironment. Si la méthode setEnv(..) est appelée plusieurs fois, lorsque les clés sont extraites du theEnvironment map, ce sont maintenant des chaînes (ayant été placées comme des chaînes par la première invocation de setEnv(...)) et ne peuvent pas être converties dans le type générique de la carte, Variable, qui est une classe interne privée de ProcessEnvironment.

Une version corrigée (en Scala) est ci-dessous. Espérons que ce ne soit pas trop difficile à reporter en 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()
  }
}
3
Paul Blair

Ceci est la version Kotlin du mal du @ pushy réponse =)

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

Cela fonctionne au moins dans macOS Mojave.

0
GarouDan

Si vous travaillez avec Spring Boot, vous pouvez ajouter la spécification de la variable d'environnement dans la propriété suivante:

was.app.config.properties.toSystemProperties
0
Alex