CodeGym /Cursos /Spring Boot /Fail-fast y StartupSummaryRunner

Fail-fast y StartupSummaryRunner

Spring Boot
Nivel 6, Lección 4
Disponible

1. Fail-fast: fallar de inmediato y de forma clara

Si estás empezando, existe un deseo muy humano: «Con tal de que arranque, luego ya veremos». El problema es que una aplicación servidora que más o menos ha arrancado, pero en realidad funciona incorrectamente, es el peor escenario. Ya está recibiendo peticiones, ya está escribiendo logs, ya está consumiendo recursos… y al mismo tiempo puede estar generando datos basura o fallando en la primera acción real del usuario. Fail-fast es el hábito de decir: «Si algo crítico está roto, es mejor fallar de inmediato, con estrépito y de forma clara».

Fail-fast en el contexto del inicio significa una idea muy simple: tienes un conjunto mínimo de condiciones sin las cuales la aplicación no debe considerarse operativa. En cuanto detectas una violación, no te encoges de hombros ni escribes un “warning”, sino que detienes el inicio lanzando una excepción. Boot registrará honestamente que el inicio no ha tenido éxito y recibirás una señal explícita: «No se puede trabajar. A reparar».

Para apreciar la diferencia, es útil comparar fail-fast con su hermano gemelo tóxico fail-late (o «de alguna manera luego»):

Enfoque Qué ocurre ante un problema de inicio Cómo se percibe en la práctica
Fail-fast La aplicación no se vuelve “ready”, el inicio finaliza con error Ves el problema rápidamente, lo arreglas rápido, reinicias rápido
Fail-late La aplicación arranca “a medias”, falla más tarde El error aparece en un lugar inesperado, el usuario ya está sufriendo y tú estás depurando «a posteriori»

En el proyecto de aprendizaje, por supuesto, no estamos haciendo un servicio de producción para un banco (y gracias a los dioses de la JVM), pero es mejor adquirir el hábito correcto con un ejemplo sencillo. Después, cuando el proyecto se haga más grande, este hábito literalmente te ahorrará horas de vida.

2. Runner al final del inicio: resumen y comprobaciones

Para que fail-fast no sea una «magia de excepciones» sino una herramienta consciente, es importante entender el lugar del runner en toda la secuencia de inicio (startup sequence). Los runners se invocan cuando Spring ya ha montado el contenedor, creado los beans e inyectado las dependencias. Es decir, puedes inyectar tranquilamente ApplicationContext, Environment, ApplicationArguments y cualquier componente propio: ya existen. Pero aún estás en la fase de inicio: si lanzas una excepción aquí, la aplicación no se volverá ready y el evento ApplicationReadyEvent no ocurrirá.

La línea de tiempo simplificada se ve así:

sequenceDiagram
    %% El runner se ejecuta al final del inicio: el contexto ya está levantado, pero "ready" aún no ha llegado
    participant JVM as "JVM/main()"
    participant Boot as "SpringApplication.run()"
    participant Ctx as "ApplicationContext"
    participant R as "Runners"
    participant App as "Aplicación lista (ready)"

    JVM->>Boot: inicio
    Boot->>Ctx: crear/refrescar contexto
    Ctx-->>Boot: contexto levantado
    Boot->>R: invocar ApplicationRunner/CommandLineRunner
    alt comprobación fallida
        R-->>Boot: "lanzar excepción (fail-fast)"
        Boot-->>JVM: ApplicationFailedEvent + finalización
    else todo correcto
        R-->>Boot: trabajo completado
        Boot-->>App: "ApplicationReadyEvent (listo)"
    end

Observa una idea muy práctica: el runner es «la última barrera antes de la disponibilidad».

Aquí es útil mantener una regla simple para elegir el lugar adecuado.

  • runner — acción única a nivel de aplicación: resumen breve, comprobación rápida, fail-fast.
  • lifecycle listener — marcador de fases started / ready / failed y diagnóstico ligero, pero no la lógica de trabajo principal.
  • @PostConstruct / @PreDestroy — preparación y limpieza local de un bean concreto, no de toda la aplicación.
  • Todo lo pesado, lento y periódico es mejor no colgarlo del camino crítico de inicio (critical startup path).

De aquí se deriva el efecto práctico: si una excepción surge de un runner, Boot registra el fallo y el ready ya no se alcanza.

Por lo tanto, el runner es ideal para dos cosas que necesitamos ahora mismo.

La primera — mostrar un breve resumen de inicio: qué aplicación es, cuántos beans tiene, qué argumentos llegaron y, en general, «hemos llegado al final del inicio». Es como un cartel de “Bienvenido al hotel”, pero sin alfombras y con beans.

La segunda — realizar comprobaciones críticas rápidas. Rápidas es la palabra clave. Si la comprobación requiere un minuto, una docena de peticiones de red y una danza chamánica alrededor de un sistema externo, entonces ya no es fail-fast, sino «fail-slow-with-drama». Necesitamos un control de calidad mínimo del inicio, sin convertir el runner en un motor de scripts oculto.

3. StartupSummaryRunner: resumen mínimo

Cuando un desarrollador ve por primera vez el log de inicio de una aplicación Boot, hay muchas líneas, la mitad de las cuales parecen «conjuros de los antiguos». Por eso, la idea de un resumen de inicio en el proyecto es muy simple: añadir unas pocas señales humanas que sean fáciles de leer a simple vista y que sean iguales en cada ejecución. No es un sustituto de los logs del framework ni un intento de «reexplicar Spring». Es una breve comprobación: «la aplicación ha llegado a la fase de runner, ve los argumentos y el contexto realmente se ha levantado».

En esta etapa del curso (mientras el proyecto aún es un “skeleton”), nos basta literalmente con unas pocas cosas. Es útil tenerlas en la cabeza como un pequeño “mapa de señales”:

Señal Cómo obtenerla Para qué saber esto
Número de definiciones de bean context.getBeanDefinitionCount() Entender rápidamente que el contexto realmente se ha levantado y que no es un «inicio vacío»
Perfiles activos (en sentido general) environment.getActiveProfiles() Ver que el entorno de inicio no es “desconocido” (incluso si no hay perfiles)
Argumentos de inicio sin procesar args.getSourceArgs() Asegurarse de que leemos los argumentos correctamente y ver con qué se ha iniciado realmente
PID del proceso (opcional) ProcessHandle.current().pid() Útil cuando ejecutas varias veces y confundes ventanas/procesos

Y ahora, lo que no se debe hacer en StartupSummaryRunner, de lo contrario convertirás una idea simple en un monstruo. No hay que imprimir “todo lo que encuentres”: la lista de todos los beans, todo el Environment, todas las system properties, toda la configuración de la JVM, todas las variables de entorno. En primer lugar, es ruido. En segundo lugar, es potencialmente inseguro incluso en código de aprendizaje (a veces en el env hay tokens, claves y otras cosas «no imprimibles»). En tercer lugar, afecta negativamente a la comprensión: quieres dar de 3 a 5 señales, no organizar un «festín para el depurador».

Y es mejor mantener esto en un único runner corto, en lugar de repartirlo entre varios componentes que compiten: así el inicio se lee como una política clara y única, no como un conjunto de sorpresas.

4. Implementación de StartupSummaryRunner en catalog-service

La parte agradable: ahora vamos a tomar todo lo anterior y crear un componente vivo en nuestro proyecto. Es importante mantener la disciplina de aprendizaje: una clase — una responsabilidad clara, el constructor — solo dependencias, el runner — lógica breve. No haremos un «runner-dios universal» que inicialice el mundo, explique Spring y prepare café. Haremos una cosita pequeña pero útil que se pueda leer incluso después de una noche sin dormir.

Dónde colocar la clase

Cuando el proyecto es pequeño, es muy fácil empezar a acumular todo en un solo paquete «porque es más rápido». Luego abres el proyecto una semana después y parece una caja de cables: todo funciona, pero da miedo tocarlo. Por eso, ya en la fase de skeleton es importante acostumbrarse a colocar las clases donde viven lógicamente. En nuestro proyecto existe el paquete catalog.bootstrap — es un buen lugar para los componentes de inicio que ayudan a levantar la aplicación y mostrar diagnóstico.

Para StartupSummaryRunner se vería así (aproximadamente):

com.example.catalogservice
└── catalog
    └── bootstrap
        └── StartupSummaryRunner.java

Sí, ahora parece un “exceso de orden”. Pero este orden es lo que luego salva al proyecto de convertirse en un batiburrillo.

Primero prepararemos la versión preliminar más escueta y luego la reemplazaremos inmediatamente por una más legible. Estas dos clases no son necesarias simultáneamente en el proyecto: el segundo bloque es el resumen final normal.

Versión preliminar: perfiles, args, número de beans

Haremos una implementación simple sobre ApplicationRunner para tener acceso inmediato a ApplicationArguments. Necesitaremos ApplicationContext (para conocer el número de beans) y Environment (para conocer los perfiles activos). Todo mediante constructor injection, como hemos acordado en el curso. Esta clase solo se necesita como sonda preliminar para sentir el conjunto de señales; un poco más abajo la reemplazaremos por el StartupSummaryRunner final.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component // Sonda preliminar: un poco más abajo la reemplazaremos por una versión final más legible
public class StartupSummaryProbeRunner implements ApplicationRunner {

    private final ApplicationContext context; // Necesaria para la señal de diagnóstico: cuántos beans se levantaron
    private final Environment environment;    // Necesaria para ver los perfiles activos (si los hay)

    public StartupSummaryProbeRunner(ApplicationContext context, Environment environment) {
        // Constructor solo para dependencias, sin "lógica de inicio"
        this.context = context;
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Este método se invoca cuando el contexto ya está levantado, pero la aplicación aún no se considera "ready"
        System.out.println("Definiciones de bean: " + context.getBeanDefinitionCount()); // Ejemplo: Definiciones de bean: 128
        System.out.println("Cantidad de args sin procesar: " + args.getSourceArgs().length);        // Ejemplo: Cantidad de args sin procesar: 0
        System.out.println("Perfiles activos: " + environment.getActiveProfiles().length); // Ejemplo: Perfiles activos: 0
    }
}

Versión final: título, PID y formato

Las líneas sueltas como Definiciones de bean: 128 ya no están mal, pero en los logs a menudo se desea un «marco»: que se vea que es nuestro resumen y no un println() aleatorio de otro lugar. Añadamos un título simple y el PID del proceso. El PID no es obligatorio, pero ayuda mucho si reinicias la aplicación con frecuencia y confundes qué ventana corresponde a qué ejecución.

Ahora reemplazamos la sonda anterior por el StartupSummaryRunner normal. Esta versión ya tiene sentido dejarla en el proyecto como resumen de inicio básico.

import java.util.Arrays;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component // Este runner es el resumen de inicio normal y permanente del proyecto
public class StartupSummaryRunner implements ApplicationRunner {

    private final ApplicationContext context; // Señal rápida: el contexto se levantó y realmente tiene beans
    private final Environment environment;    // Imprimimos los perfiles activos de forma legible

    public StartupSummaryRunner(ApplicationContext context, Environment environment) {
        this.context = context;
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        long pid = ProcessHandle.current().pid(); // El PID ayuda a distinguir diferentes ejecuciones en diferentes ventanas/terminales

        System.out.println("=== resumen de inicio de catalog-service ==="); // Título para que el resumen sea visible en los logs
        System.out.println("pid = " + pid); // Ejemplo: pid = 12345
        System.out.println("definiciones de bean = " + context.getBeanDefinitionCount()); // Ejemplo: definiciones de bean = 128
        System.out.println("args = " + Arrays.toString(args.getSourceArgs())); // Ejemplo: args = []
        System.out.println("perfiles = " + Arrays.toString(environment.getActiveProfiles())); // Ejemplo: perfiles = []
    }
}

A partir de aquí es más conveniente evolucionar esta misma clase: si se desea añadir otra señal de diagnóstico, es mejor ampliarla, no crear un segundo summary-runner al lado.

5. Variante didáctica de fail-fast: --mode obligatorio

Ahora añadamos eso de «fallar con elegancia». Elegiremos un escenario didáctico simple: la aplicación espera un argumento de opción --mode. Por ejemplo, puedes ejecutarlo así: --mode=demo. Si --mode no se pasa o se pasa sin valor, consideramos que es un error crítico de inicio y lanzamos una excepción.

¿Por qué es un buen ejemplo didáctico? Porque no requiere archivos de configuración, ni llamadas de red, ni recursos externos. Se ajusta completamente al material del día de hoy: ApplicationArguments, argumentos de opción y runner.

Es importante no confundir esto con el nuevo comportamiento permanente de catalog-service. El resumen normal de la sección anterior sigue siendo la variante normal del proyecto. A continuación, una variante de runner didáctica separada, que es más conveniente activar en lugar del runner de resumen normal durante el experimento, para sentir cómo Boot detiene el inicio ante un error crítico.

Hagámoslo con cuidado, con un mensaje claro:

import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component // Sonda didáctica de fail-fast: no tiene por qué permanecer en el proyecto como requisito permanente de inicio
public class RequiredModeCheckRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Comprobación crítica: sin mode la aplicación "no se considera" correctamente iniciada
        String mode = readRequiredMode(args);

        System.out.println("mode = " + mode);      // Ejemplo: mode = demo
        System.out.println("Comprobaciones de inicio: OK");  // Ejemplo: Comprobaciones de inicio: OK
    }

    private String readRequiredMode(ApplicationArguments args) {
        // 1) Comprobamos que la clave se ha pasado: --mode
        if (!args.containsOption("mode")) {
            // El texto del error debe ser "utilizable": que se entienda inmediatamente cómo ejecutar correctamente
            throw new IllegalStateException("Falta la opción de inicio obligatoria: --mode=demo");
        }

        // 2) Comprobamos que la clave tiene un valor: --mode=...
        List<String> values = args.getOptionValues("mode");
        if (values == null || values.isEmpty() || values.get(0).isBlank()) {
            throw new IllegalStateException("La opción de inicio --mode debe tener un valor. Ejemplo: --mode=demo");
        }

        // 3) En el ejemplo didáctico tomamos el primer valor (si se pasan varios, es una simplificación intencionada)
        return values.get(0).trim();
    }
}

Aquí hay varios puntos importantes que los principiantes suelen pasar por alto.

En primer lugar, containsOption("mode") comprueba la presencia de la clave --mode. Pero la presencia de la clave no garantiza que tenga un valor normal. Por eso, adicionalmente leemos getOptionValues("mode") y comprobamos null, la lista vacía y isBlank().

En segundo lugar, lanzamos IllegalStateException con un texto claro. No “mode error”, ni “something wrong”, sino una frase que dice directamente lo que hay que hacer. Esto se llama un “mensaje que se puede usar”. Puedes tener el stack-trace más bonito, pero si el texto del error no indica cómo corregir el inicio, no es fail-fast, es fail-annoying.

En tercer lugar, no capturamos la excepción. Y esto es fundamental. Fail-fast solo funciona cuando el error realmente detiene el inicio. Si lo envuelves en un try/catch y haces como si todo estuviera bien, vuelves al mundo de fail-late, solo que con un autoengaño adicional.

En el proyecto de aprendizaje, esto es solo una forma conveniente de ver fail-fast en vivo; después de tal experimento, catalog-service no está obligado a exigir para siempre --mode en cada ejecución.

Un antiejemplo (no se debe hacer así) se vería más o menos así:

try {
    readRequiredMode(args); // Como que "comprobamos", pero luego lo estropearemos todo
} catch (Exception e) {
    System.out.println("Vaya: " + e.getMessage()); // Informamos del error...
    // ...y continuamos el inicio como si nada (mala idea): la aplicación quedará "semi-funcional"
}

Esto es un «inicio exitoso de una aplicación rota». Sí, arrancará. Sí, incluso verás un warning. Pero si mode es realmente crítico para el comportamiento, solo estarás posponiendo el problema para más tarde, y aparecerá donde menos quieras verlo.

6. Límites del runner: sin «segunda fase»

El runner es un punto de extensión muy potente, y precisamente por eso se convierte muy fácilmente en un lugar donde se «mete de todo». Un poco de lógica, un poco de comprobación, un poco de inicialización, luego «y vamos a añadir otra llamadita»… y ya has escrito la mitad de la aplicación en un único método run(). Para que esto no ocurra, es útil mantener un marco simple: el runner debe ser corto, predecible y rápido.

Si hablamos de forma muy práctica, el runner suele hacer una de dos cosas. O imprime un resumen de diagnóstico para que se vea qué se ha iniciado exactamente. O realiza comprobaciones rápidas y hace fallar el inicio ante problemas críticos. A veces ambas cosas se combinan, pero entonces deben seguir siendo cortas y obvias, como en los ejemplos anteriores.

Otro matiz: el runner no es el lugar para una «inicialización pesada». Si dentro de run() haces un trabajo largo, alargas el inicio de la aplicación. En un proyecto de aprendizaje puede ser simplemente molesto, pero en un servicio real se convierte en un problema de operación: el despliegue es lento, el reinicio es largo, el diagnóstico es complejo. Todo lo pesado o no debería vivir en el inicio, o requiere otro mecanismo que no esté en el camino crítico de inicio (critical startup path).

Y por último: si tienes varios runners, el orden de su invocación puede no ser obvio. En las primeras etapas del curso es mejor tener un único runner explícito con resumen y comprobaciones, que repartir por el proyecto cinco “pequeños” runners diferentes y luego intentar recordar por qué imprimen líneas en un orden inesperado.

7. Errores típicos al iniciar la aplicación

A continuación, algunos errores que se encuentran con más frecuencia cuando la gente empieza a escribir lógica de inicio por primera vez. Es fácil cometerlos “por costumbre”, y luego es muy difícil explicar por qué la aplicación se comporta de forma extraña.

Error nº1: “fail-fast” solo de palabra: la excepción se captura y se ignora.
La situación más frustrante: has escrito la comprobación, incluso has lanzado la excepción… pero luego lo has envuelto todo en un try/catch y has continuado el inicio porque “bueno, que al menos arranque”. Así obtienes una aplicación que sabe informar de un error, pero no sabe reaccionar ante él. Si la condición es realmente crítica, la excepción debe detener el inicio.

Error nº2: StartupSummaryRunner se convierte en un “volcado del mundo entero”.
Existe la tentación de imprimirlo todo “para diagnóstico”: todos los argumentos, todas las variables de entorno, todos los beans, todo el contexto. El resultado, en lugar de diagnóstico, es ruido. El ojo no tiene dónde fijarse, las señales importantes se pierden y, a veces, incluso se imprimen accidentalmente valores sensibles. El resumen debe ser corto y estable, no una enciclopedia.

Error nº3: la lógica crítica de inicio se esconde en el constructor o en @PostConstruct.
El constructor de un bean es el lugar para las dependencias, no para las acciones. @PostConstruct es el ciclo de vida local de un objeto concreto, no el «inicio oficial de la aplicación». Cuando escondes ahí la lógica crítica de inicio, complicas la comprensión y el diagnóstico: quién, cuándo y por qué lo llamó. El runner hace que la lógica de inicio sea explícita y legible.

Error nº4: el runner hace un trabajo pesado y “retiene el inicio” demasiado tiempo.
Si el runner tarda segundos o minutos, pierdes la previsibilidad del inicio. Incluso en un proyecto de aprendizaje es molesto, y en un servicio real rompe el despliegue y los reinicios. Una buena comprobación de inicio o pasa rápido, o falla rápido, sin «largas reflexiones».

Error nº5: la comprobación existe, pero el mensaje de error es inútil.
throw new IllegalStateException("bad") técnicamente funciona, pero tiene casi cero valor práctico. El mensaje debe indicar qué está mal exactamente y qué hacer: por ejemplo, “Falta la opción de inicio obligatoria: --mode=demo”. Es una pequeña cosa que, en conjunto, ahorra mucho tiempo, especialmente cuando vuelves al proyecto después de un descanso.

1
Tarea
Spring Boot, nivel 6, lección 4
Bloqueada
Resumen breve de inicio mediante `StartupSummaryRunner`
Resumen breve de inicio mediante `StartupSummaryRunner`
1
Tarea
Spring Boot, nivel 6, lección 4
Bloqueada
Parámetro obligatorio `--mode` al inicio
Parámetro obligatorio `--mode` al inicio
1
Cuestionario/control
Spring Boot, nivel 6, lección 4
No disponible
Spring Boot
Arranque de la aplicación y eventos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION