CodeGym /Cursos /Java Server /Argumentos de línea de comandos y prioridad de configurac...

Argumentos de línea de comandos y prioridad de configuración

Java Server
Nivel 20, Lección 4
Disponible

1. Args: comando sobre la configuración ya ensamblada

En este punto, el effective config ya está ensamblado: application.properties se lee del classpath, env si es necesario sobrescribe valores individuales, y a la salida obtenemos un AppConfig tipificado. args resuelve otra cuestión: qué escenario ejecutamos ahora mismo.

Para ReadLater Starter esta diferencia es muy práctica: server.port, catalog.api.mode y catalog.api.base-url viven en la configuración. Mientras que catalog search clean code o catalog details OL12345M son datos de una ejecución concreta. Si se mezclan estos dos ejes, main() muy rápidamente se convertirá en una CLI-combinada donde no está claro qué reconfigura el entorno y qué selecciona el comando mismo.

2. Modos de lanzamiento canónicos

Cuando la aplicación admite varios modos, lo más valioso es acordar una forma canónica de lanzamiento. No «más o menos así», no «yo en el IDE con un botón», sino que se pueda escribir una línea en el README y que funcione para todos. Si alguna vez has ejecutado un proyecto ajeno donde «cómo ejecutar» está descrito como «bueno, tú mismo averiguas», entiendes por qué esto es importante.

Tenemos fijados tres modos de lanzamiento, y no inventamos un cuarto «por si acaso», porque esa es la forma favorita de los principiantes de complicarse la vida.

Escenario Significado Ejemplo de ejecución con Gradle
server Levanta un servidor HTTP local ./gradlew run --args="server"
catalog search <query...> Busca libros por consulta ./gradlew run --args="catalog search clean code"
catalog details <externalId> Obtiene detalles por ID externo ./gradlew run --args="catalog details OL12345M"

Observa un detalle: una consulta de búsqueda puede consistir lógicamente en varias palabras. Y en el ejemplo catalog search clean code Gradle pasará a main(String[] args) cuatro elementos: ["catalog", "search", "clean", "code"]. Si escribes un analizador «estrictamente de 3 argumentos», limitarás accidentalmente la búsqueda a una palabra. Y luego un estudiante o tú mismo estaréis 20 minutos pensando por qué «clean code» se busca como «clean», y por qué «code» se perdió. Spoiler: no se perdió, simplemente está en args[3].

Por lo tanto, consideraremos correcta la siguiente regla: catalog search acepta al menos un trozo de consulta, y si hay más trozos, los unimos de nuevo con espacios. No es un «analizador CLI complejo», es simplemente una muestra de respeto a la realidad.

Otro punto importante: args no es el lugar para elegir el puerto del servidor y el modo real/mock en esta versión de la aplicación. El puerto y el modo son configuración, y hoy ya hemos acordado dónde viven: en application.properties con posibilidad de override mediante variables de entorno. Si empiezas a asignar el puerto a veces con args, a veces con env, a veces con un archivo, crearás tres formas de hacer lo mismo, y todas entrarán en conflicto. Es como tener tres despertadores en el teléfono y sorprenderte de que aún así no duermes lo suficiente.

3. Analizador de args: String[] → LaunchCommand

Los estudiantes suelen hacer esto: en algún lugar profundo del código escriben if (args[0].equals("server")), luego en otro sitio if (args[1].equals("search")), y luego se sorprenden de que la aplicación falle con ArrayIndexOutOfBoundsException al ejecutar sin argumentos. Es normal: en algún momento todos creímos en un mundo amable donde el usuario siempre introduce todo correctamente. Pero el pensamiento backend trata precisamente de que el mundo no es amable, sino simplemente complejo.

Nuestro objetivo es convertir el array sin procesar de cadenas String[] args en un comando tipificado que luego sea fácil de manejar sin constantes args.length e índices mágicos. Para ello introducimos un tipo simple LaunchCommand. A nivel de paquete, es lógico mantenerlo en com.example.readlater.app, porque se trata de la ejecución de la aplicación, no del catálogo ni de la lista de lectura.

La versión mínima — plain Java, sin complicaciones — puede verse así:

// Tipo único para todas las variantes de lanzamiento de la aplicación.
// Esto permite trabajar con comandos como objetos, no con cadenas "mágicas".
public sealed interface LaunchCommand
        permits ServerCommand, CatalogSearchCommand, CatalogDetailsCommand {}

// Comando: levantar servidor local.
public record ServerCommand() implements LaunchCommand {}

// Comando: búsqueda en el catálogo (consulta ya unida en una cadena).
public record CatalogSearchCommand(String query) implements LaunchCommand {}

// Comando: detalles del libro por identificador externo.
public record CatalogDetailsCommand(String externalId) implements LaunchCommand {}

El sentido aquí no es «oh, qué modernos somos, sealed interface». El sentido es otro: más adelante en el código trabajaremos no con cadenas server/catalog, sino con objetos claros. Y esto reduce drásticamente la cantidad de errores.

Ahora necesitamos un analizador. No un framework separado, no una biblioteca, sino una función honesta que verifica la forma del comando y o bien devuelve LaunchCommand, o falla con un mensaje claro «cómo es correcto».

import java.util.Arrays;

public final class LaunchCommandParser {
    public static LaunchCommand parse(String[] args) {
        // 1) servidor
        if (args.length == 1 && "server".equals(args[0])) {
            return new ServerCommand();
        }

        // 2) catalog search <query...>
        // Importante: la consulta puede tener varias palabras, por lo tanto tomamos la "cola" del array y la unimos con espacios.
        if (args.length >= 3 && "catalog".equals(args[0]) && "search".equals(args[1])) {
            String query = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
            return new CatalogSearchCommand(query);
        }

        // 3) catalog details <externalId>
        if (args.length == 3 && "catalog".equals(args[0]) && "details".equals(args[1])) {
            return new CatalogDetailsCommand(args[2]);
        }

        // Si el comando no se reconoce — detenemos el lanzamiento y mostramos cómo es correcto.
        throw new IllegalArgumentException("""
                Uso:
                  server
                  catalog search <query...>
                  catalog details <externalId>
                """.trim());
    }
}

Observa args.length >= 3 en search. Esta es nuestra compatibilidad con consultas de varias palabras. Sí, simplemente tomamos el resto del array y lo unimos con espacio. Sin magia. Solo comodidad para el usuario.

Ahora la pregunta: ¿qué hacer con la excepción? En un proyecto de aprendizaje es normal que la aplicación falle al inicio con un texto comprensible, porque ejecutar sin comando no es una situación recuperable, es una entrada incorrecta. Pero es importante que el mensaje sea legible para humanos, no «Index 0 out of bounds».

Aquí hay un ejemplo de cómo mostrar el uso con cuidado. Sí, a través de System.err.println — hoy no estamos hablando de logging:

try {
    // En la frontera de la aplicación convertimos el String[] 'crudo' en un comando tipificado.
    LaunchCommand cmd = LaunchCommandParser.parse(args);
} catch (IllegalArgumentException e) {
    // Imprimimos un aviso comprensible y detenemos el lanzamiento.
    System.err.println(e.getMessage());
    return;
}

Y aquí ya se nota el hábito backend: los datos de entrada se validan en la frontera, y al interior de la aplicación llega una estructura normalizada.

4. main() como punto de ensamblaje

En algún momento el desarrollador aprende la palabra «composición» y empieza a intentar escribir un «framework universal de lanzamiento de aplicaciones». Es divertido, pero nos mantenemos con los pies en la tierra. Nuestra tarea es más modesta y útil: hacer que main() no se convierta en un vertedero de lógica de negocio, pero que siga siendo un punto de inicio honesto (composition root) donde todo se ensambla junto.

Ahora main() ensambla dos cosas independientes: el AppConfig listo y el LaunchCommand. Y solo entonces se los entrega al runner.

Si has ido construyendo el proyecto con las lecciones anteriores, tu main() creaba ApplicationBootstrap, y este a su vez creaba CommandRouter, que analizaba args por sí mismo. Ahora las responsabilidades se reparten de otra manera: el análisis de args se ha trasladado a LaunchCommandParser, y el ensamblaje de dependencias y la selección de escenario los unimos en una sola clase — ReadLaterRunner. Recibe el AppConfig listo, ensambla los servicios (como antes ApplicationBootstrap) y, según el tipo de comando, decide qué ejecutar (como antes CommandRouter). El esqueleto puede verse así:

package com.example.readlater.app;

import com.example.readlater.config.AppConfig;

public class ReadLaterRunner {
    private final AppConfig config;

    public ReadLaterRunner(AppConfig config) {
        this.config = config;
        // aquí ensamblamos servicios y controladores — lo que antes hacía ApplicationBootstrap
    }

    public void run(LaunchCommand cmd) {
        // selección de escenario — lo que antes hacía CommandRouter
        switch (cmd) {
            case ServerCommand s -> startServer();
            case CatalogSearchCommand c -> searchCatalog(c.query());
            case CatalogDetailsCommand c -> showDetails(c.externalId());
        }
    }

    private void startServer() { /* … */ }
    private void searchCatalog(String query) { /* … */ }
    private void showDetails(String externalId) { /* … */ }
}

En las siguientes lecciones mostraremos main() de forma abreviada y nos centraremos en otras cosas (logging, errores), así que allí el runner no se mencionará por separado — su papel no cambia por ello.

Un ejemplo de «ensamblaje» podría verse así:

package com.example.readlater.app;

import com.example.readlater.config.AppConfig;
import com.example.readlater.config.AppConfigFactory;
import com.example.readlater.config.PropertiesLoader;

import java.util.Properties;

public class ReadLaterApplication {
    public static void main(String[] args) {
        Properties props = new PropertiesLoader().load();
        AppConfig config = AppConfigFactory.from(props, System.getenv());

        LaunchCommand cmd;
        try {
            cmd = LaunchCommandParser.parse(args);
        } catch (IllegalArgumentException e) {
            System.err.println(e.getMessage());
            return;
        }

        ReadLaterRunner runner = new ReadLaterRunner(config);
        runner.run(cmd);
    }
}

Observa que en este fragmento no hay ni una línea como «elige server.port de env» o «parsea catalog.api.mode». Todo eso ya lo hizo la capa de configuración. main() solo conecta el AppConfig listo con el comando listo.

Para que el cerebro acepte definitivamente el panorama, es útil dibujar un diagrama de lanzamiento. Me gusta mermaid porque es más barato que «dibujar en la cabeza y sufrir».

flowchart TD
    A["Inicio de la aplicación (main)"] --> B["PropertiesLoader"]
    B --> C["Properties"]
    C --> D["AppConfigFactory.from(props, System.getenv())"]
    D --> E["AppConfig"]

    A --> F["LaunchCommandParser.parse(args)"]
    F --> G["LaunchCommand: server / catalog search / catalog details"]

    E --> H["ReadLaterRunner"]
    G --> H

    H --> I{"Selección de escenario"}
    I -->|"server"| J["Server mode"]
    I -->|"catalog search"| K["Catalog search"]
    I -->|"catalog details"| L["Catalog details"]

Así main() sigue siendo la puerta de entrada de la aplicación, no el lugar donde de repente vive toda la lógica del mundo.

5. El orden importa: primero AppConfig, luego el comando

La regla de prioridad de valores ya está fijada en la capa de configuración: env → application.properties → default. Aquí solo importa una cosa: LaunchCommandParser.parse(args) no participa en esta cadena. En el momento de analizar args, server.port, catalog.api.mode y otras configuraciones ya se han convertido en un AppConfig effective.

Por lo tanto, en main() es conveniente mantener este orden: primero PropertiesLoader, luego AppConfigFactory.from(props, System.getenv()), y solo después LaunchCommandParser.parse(args). Así en el código se ven inmediatamente dos ejes separados: el entorno se ensambla una vez, luego se elige el comando.

6. Errores típicos con args y configuración

Aquí normalmente se quiere hacer una lista de 12 puntos, pero acordamos mantener la narración fluida, como un build.gradle.kts bien pulido — bueno, casi. Los errores a continuación no son «ustedes son malos», es «casi todos hacen esto hasta que se golpean». Nuestra tarea es golpearnos virtualmente y por adelantado.

Error nº1: mezclan el modo de lanzamiento y la configuración, convirtiendo args en un vertedero.
Cuando empiezas a pasar a través de args tanto server como port como baseUrl como timeouts, la aplicación se vuelve impredecible: una parte de la configuración cambia mediante env, otra parte mediante archivo, otra parte mediante args, y no hay una única «fuente de verdad». En el curso es mejor mantener una regla férrea: args describe el comando y los datos de ejecución, la configuración vive en properties/env.

Error nº2: analizan args «en el lugar de uso» y atrapan ArrayIndexOutOfBoundsException.
Si args[1] se lee en un método, args[2] en otro, y la comprobación de args.length se olvidó en un tercero, entonces seguro que obtendrás un fallo en alguna ejecución no estándar. La cura es simple: un único analizador en la entrada, que o bien devuelve LaunchCommand o bien detiene el lanzamiento con un usage comprensible.

Error nº3: hacen catalog search estrictamente de tres argumentos y accidentalmente rompen consultas de varias palabras.
Es un bug sutil porque no parece un error: la aplicación «funciona», pero busca algo diferente. Si tienes catalog search clean code y el analizador solo toma args[2], en realidad estás buscando «clean». La solución es aceptar args.length >= 3 y unir el resto de la consulta con espacios.

Error nº4: eligen la prioridad de las fuentes de manera diferente en distintos lugares.
Hoy escribiste «env es más importante que el archivo», mañana en otra clase hiciste lo contrario, pasado mañana agregaste un default «por si acaso». Resultado: estableces una variable de entorno y no funciona porque esa clave en particular se lee de otra manera. La cura: una regla en ConfigReader o AppConfigFactory, no System.getenv() y props.getProperty(...) locales por todo el código.

Error nº5: reemplazan un comando desconocido con un valor por defecto.
A veces hacen así: «si el comando no está claro, ejecutamos server». Parece conveniente, hasta que un día ejecutas ./gradlew run --args="sevrer" y te quedas 10 minutos mirando la terminal vacía, preguntándote por qué «no pasa nada». Un modo de lanzamiento desconocido es un error, y debe ser explícito.

1
Tarea
Java Server, nivel 20, lección 4
Bloqueada
`LaunchCommandParser` para tres modos de inicio
`LaunchCommandParser` para tres modos de inicio
1
Tarea
Java Server, nivel 20, lección 4
Bloqueada
Resumen de inicio desde `AppConfig` y `LaunchCommand`
Resumen de inicio desde `AppConfig` y `LaunchCommand`
1
Cuestionario/control
Configuración del proyecto, nivel 20, lección 4
No disponible
Configuración del proyecto
Archivos y prioridades
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION