Pregunta

¿Cómo configuro las variables de entorno de Java? Veo que puedo hacer esto para subprocesos usando ProcessBuilder . Sin embargo, tengo varios subprocesos para comenzar, así que prefiero modificar el entorno del proceso actual y dejar que los subprocesos lo hereden.

Hay un System.getenv(String) para obtener una sola variable de entorno. También puedo obtener un Map del conjunto completo de variables de entorno con System.getenv(). Pero llamar a put() en ese UnsupportedOperationException arroja un System.setenv(), aparentemente significa que el entorno es de solo lectura. Y, no hay <=>.

Entonces, ¿hay alguna forma de establecer variables de entorno en el proceso actualmente en ejecución? ¿Si es así, cómo? Si no, ¿cuál es la razón? (¿Es porque esto es Java y, por lo tanto, no debería estar haciendo cosas obsoletas mal portables como tocar mi entorno?) Y si no, alguna buena sugerencia para administrar los cambios de las variables de entorno que voy a necesitar alimentar a varios subprocesos?

¿Fue útil?

Solución

  

(¿Es porque esto es Java y, por lo tanto, no debería estar haciendo cosas obsoletas mal portables como tocar mi entorno?)

Creo que has dado en el clavo.

Una posible forma de aliviar la carga sería factorizar un método

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

y pase cualquier ProcessBuilder s antes de comenzar.

Además, probablemente ya lo sepas, pero puedes comenzar más de un proceso con el mismo <=>. Entonces, si sus subprocesos son los mismos, no necesita hacer esta configuración una y otra vez.

Otros consejos

Para usar en escenarios en los que necesita establecer valores de entorno específicos para pruebas unitarias, puede encontrar útil el siguiente truco. Cambiará las variables de entorno en toda la JVM (así que asegúrese de restablecer cualquier cambio después de su prueba), pero no alterará el entorno de su sistema.

Descubrí que una combinación de los dos trucos sucios de Edward Campbell y anónimo funciona mejor, ya que uno de ellos no funciona en Linux, uno no funciona en Windows 7. Entonces, para obtener un truco malvado multiplataforma, los combiné:

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

Esto funciona como un encanto. Créditos completos a los dos autores de estos hacks.

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 para agregar / actualizar una única var y eliminar el bucle según la sugerencia 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);
  }
// 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);
}

en Android, la interfaz se expone a través de Libcore.os como una especie de API oculta.

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

La clase Libcore y el sistema operativo de la interfaz son públicos. Solo falta la declaración de clase y debe mostrarse al vinculador. No es necesario agregar las clases a la aplicación, pero tampoco hace daño si se incluye.

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

Configuración de variables de entorno individuales (según la respuesta de 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);
    }
}

Uso :

Primero, coloque el método en la clase que desee, p. SystemUtil.

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

Si llama a System.getenv("SHELL") después de esto, obtendrá "/bin/bash" de vuelta.

Resulta que la solución de @ pushy / @ anonymous / @ Edward Campbell no funciona en Android porque Android no es realmente Java. Específicamente, Android no tiene java.lang.ProcessEnvironment en absoluto. Pero resulta ser más fácil en Android, solo necesita hacer una llamada JNI a 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;
}

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

Esta es una combinación de la respuesta de @ paul-blair convertida a Java que incluye algunas limpiezas señaladas por paul blair y algunos errores que parecen haber estado dentro del código de @pushy que se compone de @Edward Campbell y anónimo.

No puedo enfatizar cuánto se debe usar SOLO este código en las pruebas y es extremadamente hacky. Pero para los casos en que necesita la configuración del entorno en las pruebas, es exactamente lo que necesitaba.

Esto también incluye algunos toques míos menores que permiten que el código funcione tanto en Windows que se ejecuta en

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í como Centos ejecutándose en

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 implementación:

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

Buscando en línea, parece que podría ser posible hacer esto con JNI. Entonces tendría que hacer una llamada a putenv () desde C, y (presumiblemente) tendría que hacerlo de una manera que funcionara tanto en Windows como en UNIX.

Si se puede hacer todo eso, seguramente no sería demasiado difícil para Java soportar esto en lugar de ponerme una camisa de fuerza.

Un amigo que habla Perl en otra parte sugiere que esto se debe a que las variables de entorno son procesos globales y Java se esfuerza por un buen aislamiento para un buen diseño.

Intenté la respuesta de Pushy arriba y funcionó en su mayor parte. Sin embargo, en ciertas circunstancias, vería esta excepción:

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

Esto sucede cuando el método se llamó más de una vez, debido a la implementación de ciertas clases internas de ProcessEnvironment. Si el método setEnv(..) se llama más de una vez, cuando las claves se recuperan de <= > mapa, ahora son cadenas (habiendo sido puestas como cadenas por la primera invocación de theEnvironment) y no se pueden convertir al tipo genérico del mapa, setEnv(...) que es una clase interna privada de Variable,

Una versión fija (en Scala), está debajo. Esperemos que no sea demasiado difícil de transferir a 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()
  }
}

Como la mayoría de las personas que han encontrado este hilo, estaba escribiendo algunas pruebas unitarias y necesitaba modificar las variables de entorno para establecer las condiciones correctas para que se ejecutara la prueba. Sin embargo, descubrí que las respuestas más votadas tenían algunos problemas y / o eran muy crípticas o demasiado complicadas. Esperemos que esto ayude a otros a resolver la solución más rápidamente.

Primero, finalmente encontré que la solución de @Hubert Grzeskowiak era la más simple y funcionó para mí. Desearía haber venido a eso primero. Se basa en la respuesta de @Edward Campbell, pero sin la complicación de la búsqueda de bucle.

Sin embargo, comencé con la solución de @ pushy, que obtuvo la mayor cantidad de votos. Es una combinación de @anonymous y @Edward Campbell's. @pushy afirma que ambos enfoques son necesarios para cubrir los entornos de Linux y Windows. Estoy ejecutando bajo OS X y encuentro que ambos funcionan (una vez que se soluciona un problema con el enfoque @anonymous). Como otros han señalado, esta solución funciona la mayor parte del tiempo, pero no todo.

Creo que la fuente de la mayor parte de la confusión proviene de la solución de @ anonymous que opera en el campo 'theEnvironment'. Mirando la definición de ProcessEnvironment estructura, 'theEnvironment' no es un Mapa < Cadena, Cadena & Gt; sino que es un Mapa < Variable, Valor & Gt ;. Borrar el mapa funciona bien, pero la operación putAll reconstruye el mapa a Map & Lt; String, String & Gt ;, que potencialmente causa problemas cuando las operaciones posteriores operan en la estructura de datos utilizando la API normal que espera Map & Lt; Variable, Valor & Gt ;. Además, acceder / eliminar elementos individuales es un problema. La solución es acceder indirectamente al 'entorno' a través del 'entorno no modificable'. Pero dado que este es un tipo UnmodifiableMap el acceso debe hacerse a través de la variable privada 'm' del tipo UnmodifiableMap. Vea getModifiableEnvironmentMap2 en el código a continuación.

En mi caso, necesitaba eliminar algunas de las variables de entorno para mi prueba (las otras no deberían modificarse). Luego quise restaurar las variables de entorno a su estado anterior después de la prueba. Las rutinas a continuación lo hacen sencillo. Probé ambas versiones de getModifiableEnvironmentMap en OS X, y ambas funcionan de manera equivalente. Aunque se basa en los comentarios de este hilo, uno puede ser una mejor opción que el otro dependiendo del entorno.

Nota: No incluí el acceso al 'theCaseInsensitiveEnvironmentField' ya que parece ser específico de Windows y no tenía forma de probarlo, pero agregarlo debería ser sencillo.

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

Esta es la versión maligna de Kotlin de la maldad de @ pushy respuesta =)

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

Al menos funciona en macOS Mojave.

Implementación de Kotlin que hice recientemente en base a la respuesta de 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)
    }
}

Puede pasar parámetros a su proceso inicial de Java con -D:

java -cp <classpath> -Dkey1=value -Dkey2=value ...
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top