CodeGym /Cursos /Java Server /Variables de entorno: anulación externa

Variables de entorno: anulación externa

Java Server
Nivel 20, Lección 3
Disponible

1. Variables de entorno sobre application.properties

Si ya tienes un application.properties, es fácil pensar que es suficiente: «bueno, puedo cambiar el puerto ahí y listo». El problema es que el archivo es parte del proyecto, mientras que el entorno es parte de la ejecución. Y cuando empiezas a ejecutar el mismo artefacto en diferentes condiciones, necesitas poder cambiar los valores «desde fuera» sin tocar el repositorio ni crear commits del tipo «cambié temporalmente a 9090, y lo devolveré, lo prometo».

Imagina que application.properties es una receta de sopa que está en el libro de familia. Es común y más o menos estable. Las variables de entorno son la «sal al gusto» directamente en la cocina: hoy le pones mucha sal, mañana poca, pero no reescribes la receta ni imprimes una nueva edición del libro.

Ya tenemos AppConfig: el archivo se lee desde el classpath, las cadenas se convierten en tipos, las claves obligatorias se verifican al inicio. Ahora no cambiamos el modelo. Solo cambia la cuestión: ¿de dónde proviene el valor efectivo si en una ejecución concreta se necesita anular el archivo desde fuera?

En nuestro proyecto educativo esto es especialmente útil para cosas como catalog.api.mode (cambiar rápidamente de real a mock), catalog.api.base-url (el proveedor cambió o estás probando otra dirección) y server.port (el puerto suele entrar en conflicto con algo ya ejecutándose en la máquina).

2. Cómo ve Java una variable de entorno

Una variable de entorno es un par NOMBRE=VALOR que el sistema operativo pasa al proceso al iniciarlo. Esto es importante: las variables de entorno no «viven en Java», viven en el entorno del proceso. Java simplemente le pregunta al SO: «¿Qué valores me dieron cuando me ejecutaron?».

En Java esto se hace mediante System.getenv(). Este método devuelve un mapa donde las claves son los nombres de las variables de entorno y los valores son cadenas. Y aquí volvemos a encontrarnos con la idea familiar de la conferencia sobre Properties: casi toda la configuración llega al mundo como cadenas. Incluso si es un «puerto» que consideras un número, desde fuera sigue siendo texto "8080".

Un mini ejemplo de lectura de una variable de entorno sería:

// Leemos la variable de entorno que el SO pasó al proceso al iniciar
String port = System.getenv("READLATER_SERVER_PORT");

// Importante: si la variable no está establecida, aquí será null
System.out.println(port); // por ejemplo: 9090 (o null si la variable no está establecida)

Aquí hay dos sutilezas que realmente duelen en la práctica. La primera: la variable puede no estar establecida, y entonces obtienes null. La segunda: la variable puede estar establecida pero ser una cadena vacía (por ejemplo, alguien «la anuló» pero no especificó el valor), y esto casi siempre debe tratarse como «no establecida».

3. Nombres «torcidos» de variables de entorno

Cuando te acostumbras a claves como catalog.api.base-url, quieres tener exactamente la misma clave en el entorno. Pero las variables de entorno casi siempre han vivido históricamente en un mundo donde los puntos y los guiones son una mala idea (y en algunos lugares simplemente están prohibidos). Por eso, para las variables de entorno se suele usar el estilo UPPER_SNAKE_CASE: letras mayúsculas y guiones bajos.

Y aquí surge una importante tarea de ingeniería: no crear dos universos de configuración independientes donde en el archivo sea una cosa, en el entorno otra, y nadie recuerde cómo se relacionan. Necesitamos un esquema claro. Para el proyecto ReadLater Starter es conveniente acordar que todas las variables de entorno comiencen con el prefijo READLATER_ y luego repitan el significado de la clave de properties.

Podemos fijar el esquema con una tabla. La hago intencionadamente «aburrida»: las reglas aburridas se rompen menos.

Clave en application.properties variable de entorno ejemplo de valor
app.name READLATER_APP_NAME readlater
catalog.api.base-url READLATER_CATALOG_API_BASE_URL https://openlibrary.org
catalog.api.mode READLATER_CATALOG_API_MODE mock
catalog.api.request-timeout-ms READLATER_CATALOG_API_REQUEST_TIMEOUT_MS 2500
server.host READLATER_SERVER_HOST localhost
server.port READLATER_SERVER_PORT 9090

Observa: no intentamos construir los nombres automáticamente de forma demasiado inteligente. Sí, se podría escribir un generador que reemplace puntos/guiones por _, los ponga en mayúsculas y añada el prefijo. Pero para el aprendizaje es más importante que veas la correspondencia con tus ojos y entiendas lo que ocurre. La automatización se puede añadir después, cuando te aburras (el aburrimiento es el mejor indicador de que ya has entendido el tema).

4. Regla de anulación: la variable de entorno es más importante que el archivo

La configuración es como un pastel de capas. Y lo más desagradable que puede pasar es que en diferentes partes de la aplicación el pastel se «estratifique» de manera diferente. En un lugar tomas el valor del archivo, en otro del entorno, en otro del valor por defecto, y al final tu aplicación se comporta como un gato: parece doméstico, pero vive según sus propias reglas.

Por eso necesitamos una regla de prioridad clara que se aplique en un solo lugar:

Variable de entorno → application.properties → valor por defecto

Esta regla debe ser exactamente así porque la variable de entorno es una anulación externa que normalmente se define «al ejecutar». El archivo es la configuración base del proyecto. El valor por defecto es un seguro para que la aplicación pueda iniciarse con valores razonables si la clave es opcional.

Es muy importante no confundir «valor por defecto» y «archivo». Si pones el valor por defecto por encima del archivo, el archivo se vuelve inútil: cambiarás application.properties pero la application se quedará obstinadamente con el valor por defecto. Es uno de esos errores que luego se curan mucho tiempo y con frases como «¡pero te juro que cambié el valor!».

5. Ampliamos ConfigReader: entorno → archivo → valor por defecto

Para que la regla de prioridad no se disperse por el código, no construimos una segunda arquitectura de configuración al lado. Simplemente ampliamos ConfigReader: ahora es capaz de mirar primero en el entorno, luego en el archivo y luego devolver el valor por defecto.

package com.example.readlater.config;

import java.util.Collections;
import java.util.Map;
import java.util.Properties;

public class ConfigReader {
    private final Properties props;
    private final Map<String, String> env;

    public ConfigReader(Properties props) {
        this(props, Collections.emptyMap());
    }

    public ConfigReader(Properties props, Map<String, String> env) {
        this.props = props;
        this.env = env;
    }

    public String required(String key) {
        return required(key, null);
    }

    public String required(String key, String envKey) {
        String value = choose(key, envKey, null);
        if (value == null) {
            String suffix = envKey == null ? "" : " (env: " + envKey + ")";
            throw new ConfigException("Missing key: " + key + suffix);
        }
        return value;
    }

    public String optional(String key, String defaultValue) {
        return optional(key, null, defaultValue);
    }

    public String optional(String key, String envKey, String defaultValue) {
        String value = choose(key, envKey, defaultValue);
        return value == null ? defaultValue : value;
    }

    private String choose(String key, String envKey, String defaultValue) {
        if (envKey != null) {
            String envValue = env.get(envKey);
            if (envValue != null && !envValue.isBlank()) return envValue.trim();
        }

        String fileValue = props.getProperty(key);
        if (fileValue != null && !fileValue.isBlank()) return fileValue.trim();

        return defaultValue;
    }
}

Aquí ocurrió exactamente lo que necesitábamos: la lógica anterior de lectura del archivo no ha desaparecido, simplemente apareció una capa externa de anulación. Si la variable de entorno no está definida, ConfigReader se comporta igual que antes.

6. Los valores obligatorios siguen siendo obligatorios

La anulación mediante entorno no hace que lo que es necesario para que la aplicación se inicie de forma significativa se vuelva «menos obligatorio». Para catalog.api.base-url la regla sigue siendo la misma: primero probamos el entorno, luego el archivo, y si no se encuentra en ninguno, detenemos el inicio.

ConfigReader r = new ConfigReader(props, System.getenv());

// env -> archivo -> error
String baseUrl = r.required("catalog.api.base-url", "READLATER_CATALOG_API_BASE_URL");

// env -> archivo -> valor por defecto
String mode = r.optional("catalog.api.mode", "READLATER_CATALOG_API_MODE", "mock");

El sentido aquí es simple: el entorno proporciona un lugar más de donde se puede tomar el valor, pero no cambia la política en sí. Una clave obligatoria sigue siendo obligatoria, una opcional sigue siendo opcional.

7. Elegimos la cadena aquí, los tipos se siguen recogiendo en la capa de configuración

Y otra cosa importante: la anulación mediante entorno no cancela la tipificación del paso anterior. Solo ayuda a elegir la cadena «efectiva». Luego todo sigue igual: Parse, Durations, Uris y Modes la convierten en tipos normales.

ConfigReader r = new ConfigReader(props, System.getenv());

Duration requestTimeout = Durations.msOrDefault(
        r.optional("catalog.api.request-timeout-ms", "READLATER_CATALOG_API_REQUEST_TIMEOUT_MS", "2000"),
        2000,
        "catalog.api.request-timeout-ms"
);

int port = Parse.intOrDefault(
        r.optional("server.port", "READLATER_SERVER_PORT", "8080"),
        8080,
        "server.port"
);

Lo mismo funciona con Uris.requiredUri(...) y Modes.parseMode(...): la anulación mediante entorno no es un nuevo modelo de configuración, sino simplemente una nueva fuente de la cadena antes de tipificarla.

8. Anulaciones mediante entorno en AppConfigFactory

Desde fuera no cambia nada dramático: AppConfigFactory sigue siendo el mismo punto de montaje. Solo que ahora tiene una versión que acepta el entorno y construye una configuración efectiva, no solo el contenido del archivo.

package com.example.readlater.config;

import java.util.Collections;
import java.util.Map;
import java.util.Properties;

public class AppConfigFactory {
    public static AppConfig from(Properties props) {
        return from(props, Collections.emptyMap());
    }

    public static AppConfig from(Properties props, Map<String, String> env) {
        ConfigReader r = new ConfigReader(props, env);

        CatalogConfig catalog = new CatalogConfig(
                Uris.requiredUri(
                        r.required("catalog.api.base-url", "READLATER_CATALOG_API_BASE_URL"),
                        "catalog.api.base-url"
                ),
                Durations.msOrDefault(
                        r.optional("catalog.api.request-timeout-ms", "READLATER_CATALOG_API_REQUEST_TIMEOUT_MS", "2000"),
                        2000,
                        "catalog.api.request-timeout-ms"
                ),
                Modes.parseMode(
                        r.optional("catalog.api.mode", "READLATER_CATALOG_API_MODE", "mock"),
                        CatalogApiMode.MOCK,
                        "catalog.api.mode"
                )
        );

        ServerConfig server = new ServerConfig(
                r.optional("server.host", "READLATER_SERVER_HOST", "localhost"),
                Parse.intOrDefault(
                        r.optional("server.port", "READLATER_SERVER_PORT", "8080"),
                        8080,
                        "server.port"
                )
        );

        return new AppConfig(
                r.optional("app.name", "READLATER_APP_NAME", "readlater"),
                catalog,
                server
        );
    }
}

El modelo AppConfig aquí no ha cambiado en absoluto. CatalogConfig y ServerConfig son los mismos; solo cambia de dónde se toma el valor bruto antes del análisis.

9. Esquema de capas de configuración

A veces es más útil ver una imagen una vez que leer la palabra «prioridad» diez veces. El esquema a continuación es lo que realmente hemos construido.

flowchart TD
    Env["Variables de entorno
(READLATER_...)"] --> Reader["ConfigReader
env -> archivo -> valor por defecto"] Props["application.properties
mediante PropertiesLoader"] --> Reader Defaults["Valores por defecto
(8080, mock, 2000...)"] --> Reader Reader --> Factory["AppConfigFactory.from(props, env)"] Factory --> AppConfig["AppConfig
(listo para usar)"]

Aquí intencionadamente no hay argumentos de línea de comandos. Estos resuelven otro problema: qué escenario ejecutar. Mientras que las variables de entorno y properties resuelven el problema: con qué parámetros funciona ese escenario.

10. Ejecución sin modificar el archivo

Ahora viene el momento más agradable: sentir que todo esto no es solo «charla de arquitectura», sino que realmente funciona.

Supongamos que en application.properties tienes:

catalog.api.mode=real
server.port=8080

Pero hoy quieres trabajar en modo mock y en otro puerto (por ejemplo, para no entrar en conflicto con algo en el 8080). Puedes hacerlo sin tocar el archivo ni una sola vez.

Ejemplo para macOS/Linux

# Las variables de entorno solo actúan para esta ejecución del comando
READLATER_CATALOG_API_MODE=mock READLATER_SERVER_PORT=9090 ./gradlew run --args="catalog search clean code"

Ejemplo para Windows PowerShell

# Definimos las variables de entorno para la sesión actual de PowerShell
$env:READLATER_CATALOG_API_MODE="mock"
$env:READLATER_SERVER_PORT="9090"
./gradlew.bat run --args="catalog search clean code"

Notarás dos cosas. Primera: la aplicación en el mismo repositorio se ejecuta con diferentes configuraciones sin crear commits. Segunda: si luego cierras la terminal o restableces las variables de entorno, todo vuelve a los valores del archivo, porque la anulación mediante entorno es una anulación para la ejecución.

Para que sea aún más evidente, a veces es útil mostrar temporalmente la configuración efectiva en la consola al iniciar. En esta etapa es suficiente para verificar rápidamente que la anulación realmente funcionó.

// Obtenemos la configuración ya ensamblada (teniendo en cuenta las anulaciones por entorno)
Properties props = new PropertiesLoader().load();
AppConfig config = AppConfigFactory.from(props, System.getenv());

// Imprimimos temporalmente los valores «efectivos» para verificar que la anulación funcionó
System.out.println("catalog.mode = " + config.catalog().mode()); // catalog.mode = MOCK
System.out.println("server.port = " + config.server().port());   // server.port = 9090

Lo principal es no convertir esto en un estilo de vida permanente. println es como una tirita: ayuda a ver rápidamente dónde duele, pero no cura la causa.

11. Errores típicos al trabajar con anulaciones por entorno

Error nº 1: intentar nombrar la variable de entorno como la clave del archivo (catalog.api.base-url).
Como resultado, la variable o no se establece en absoluto, o se establece «de alguna manera», y luego Java no la ve con el nombre esperado. Acuerda usar READLATER_... y UPPER_SNAKE_CASE, y luego la vida se volverá aburrida – y el aburrimiento en la configuración es bueno.

Error nº 2: considerar una cadena vacía como un valor válido.
Una historia muy común: la variable de entorno se «anuló», pero se olvidó el valor. Si no compruebas isBlank(), la aplicación tomará la cadena vacía como una «configuración», y luego comenzarán los milagros: baseUrl vacío, host vacío, mode vacío. Es mejor tratar la cadena vacía como «variable no establecida» y lanzar un error o usar un valor de respaldo.

Error nº 3: analizar números «donde sea» y luego recibir un NumberFormatException en medio de la lógica de negocio.
Si server.port se convierte en int en algún lugar profundo del código, el error se disparará no al iniciar, sino en el momento en que ya has hecho la mitad del trabajo. Es mucho más agradable analizar los números una sola vez en la capa de configuración y obtener un mensaje claro como «Bad int for server.port».

Error nº 4: leer System.getenv() en diez lugares del proyecto.
Al principio parece conveniente, pero luego se convierte en «¿por qué en un lugar el puerto es 9090 y en otro 8080?». La regla de selección de valores debe vivir en ConfigReader / AppConfigFactory; de lo contrario, obtendrás diferentes «realidades» dentro de una misma aplicación.

Error nº 5: confundir configuración con datos de una ejecución concreta.
READLATER_SERVER_PORT es configuración. Mientras que catalog search clean code es un comando, es decir, datos de una ejecución concreta. Si empiezas a intentar pasar una consulta de búsqueda a través de una variable de entorno o, por el contrario, a definir el puerto como argumento del comando, obtendrás una mezcla extraña. Mantén en mente un modelo simple: las variables de entorno y properties configuran; los argumentos describen el escenario.

1
Tarea
Java Server, nivel 20, lección 3
Bloqueada
Puerto del servidor con prioridad `env -> file -> default`
Puerto del servidor con prioridad `env -> file -> default`
1
Tarea
Java Server, nivel 20, lección 3
Bloqueada
Sobrescritura de la dirección del catálogo y el modo mediante env
Sobrescritura de la dirección del catálogo y el modo mediante env
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION