CodeGym /Cursos /Spring Core /Inicialización eager y lazy

Inicialización eager y lazy

Spring Core
Nivel 10, Lección 3
Disponible

1. Momento de inicio del ciclo de vida de un bean

Hasta ahora, hemos considerado el ciclo de vida como una secuencia de fases: creación → inyección → init → trabajo → destroy. Pero esta cadena no está en el aire — se inicia en un momento específico. Y aquí comienza lo interesante: Spring tiene un comportamiento "por defecto" y una forma de desviarse de él. Y esto afecta tanto a la velocidad de inicio como al diagnóstico de errores.

@Lazy se percibe fácilmente como una herramienta para momentos incómodos en el wiring. Aquí importa otro ángulo: la misma anotación desplaza el momento en que el bean atraviesa su ciclo de vida.

Si lo simplificamos a una analogía doméstica, el contenedor Spring se parece a una cafetería antes de abrir. Se puede preparar todo el café de antemano, colocarlo todo en su sitio y abrir las puertas ya completamente listo (esto es eager). O se pueden abrir las puertas y luego preparar algunos productos "bajo demanda" (esto es lazy). En ambos casos obtendrá café, pero la diferencia radica en cuándo se dará cuenta de que se acabaron los vasos y se rompió el molinillo.

Aquí es importante ver en @Lazy no un "acelerador de aplicación", sino un desplazamiento del punto en el que el bean pasa por sus fases del ciclo de vida.

2. Eager por defecto

Por defecto, ApplicationContext (en nuestro curso, generalmente AnnotationConfigApplicationContext) se comporta de manera bastante directa: intenta crear todos los singleton-beans inmediatamente al iniciar. Precisamente por eso Spring ama el enfoque fail-fast: si el wiring está roto o la lógica init falla, lo sabrá no "en algún momento después", sino directamente al inicio.

Aquí nos centramos en el caso habitual — singleton-beans. Esto es suficiente para entender cómo eager/lazy desplaza el momento de inicio del ciclo de vida.

Mini demo de eager

Ahora haremos un pequeño ejemplo que se puede ejecutar diez veces — y cada vez se comportará igual. Añadiremos específicamente System.out.println(...) en el constructor y en @PostConstruct para ver ambas fases: "objeto apareció" y "objeto preparado después de la inyección de dependencias".

import jakarta.annotation.PostConstruct;

class StartupBean {

    StartupBean() {
        // Constructor: visible el momento de "creación del objeto"
        System.out.println("StartupBean: constructor"); // StartupBean: constructor
    }

    @PostConstruct
    void init() {
        // Fase init: se ejecutará después de la inyección de dependencias
        System.out.println("StartupBean: @PostConstruct"); // StartupBean: @PostConstruct
    }
}

Ahora registramos este bean a través de la configuración:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class EagerDemoConfig {

    @Bean
    StartupBean startupBean() {
        // Este método será invocado por el contenedor al crear el bean
        return new StartupBean();
    }
}

E iniciamos el contexto:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class EagerDemoApp {
    public static void main(String[] args) {
        // try-with-resources garantiza el cierre correcto del contexto (y la llamada a las fases destroy)
        try (var ctx = new AnnotationConfigApplicationContext(EagerDemoConfig.class)) {
            System.out.println("Context started"); // Context started
        }
    }
}

Si ejecuta esto, verá que los mensajes del constructor y de @PostConstruct se imprimen antes de "Context started". Esto es eager: el contenedor creó y preparó el bean al iniciar, incluso si nunca hizo ctx.getBean(StartupBean.class).

Eager inicia todo el ciclo de vida

En este punto un principiante suele pensar: "Bien, eager es simplemente cuando el objeto se crea de antemano". En la práctica, lo importante es otra cosa: eager inicia toda la cadena del ciclo de vida. Es decir, no solo new, sino también la inyección de dependencias, la fase init y las posibles comprobaciones.

Si su bean en @PostConstruct verifica la configuración y lanza una excepción, entonces eager hará que la aplicación falle al iniciar. Esto suena grave, pero en la realidad suele ser muy útil: es mejor fallar inmediatamente que después de 20 minutos de funcionamiento (especialmente si es un servicio que debe funcionar de manera estable durante horas).

3. Lazy: @Lazy y el momento de creación de un bean

Ahora añadimos la segunda estrategia. En Spring existe la anotación @Lazy, que le dice al contenedor: "No crees este bean al iniciar, hasta que sea necesario". Esto suena como el sueño de un estudiante un lunes por la mañana (lo entiendo), pero el contenedor tiene su propia lógica: si nadie ha solicitado el bean, su ciclo de vida simplemente no comienza.

Es importante entender que lazy no es un "semi-bean" separado. Es un bean normal con todas las fases del ciclo de vida, solo que estas fases comienzan más tarde, cuando alguien solicita el bean por primera vez a través de getBean(...) o cuando el contenedor lo necesita por primera vez para satisfacer una dependencia.

@Lazy en un método @Bean

Tomemos el mismo ejemplo, pero marquemos el bean como @Lazy.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
class LazyDemoConfig {

    @Bean
    @Lazy // Pedimos a Spring que retrase la creación del bean hasta la primera solicitud
    StartupBean startupBean() {
        return new StartupBean();
    }
}

Y ahora un main que primero inicia el contexto y luego solicita el bean:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class LazyDemoApp {
    public static void main(String[] args) {
        try (var ctx = new AnnotationConfigApplicationContext(LazyDemoConfig.class)) {
            // En este punto el bean aún no se ha creado (es lazy)
            System.out.println("Context started"); // Context started

            // La primera solicitud al bean inicia su creación y fase init
            ctx.getBean(StartupBean.class);
        }
    }
}

La salida será similar a:

Context started
StartupBean: constructor
StartupBean: @PostConstruct

Es decir, ahora el ciclo de vida del bean (creación, init) se ha desplazado desde el momento de inicio del contexto al momento de getBean(...).

Y aquí es importante mencionar la frase clave de esta lección. @Lazy desplaza no solo el constructor, sino también las inyecciones de dependencias y la fase init. Si quería "simplemente posponer una preparación pesada", entonces sí — la ha pospuesto. Pero también ha pospuesto la detección de posibles problemas.

@Lazy en @Component

En un proyecto real, muchos beans se registrarán mediante component scanning, es decir, a través de @Component/@Service/@Repository. @Lazy también se puede colocar en la clase.

import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Lazy // El componente se creará solo en la primera utilización real
@Component
class LazyComponent {

    LazyComponent() {
        // Aquí llegaremos solo cuando el componente sea realmente necesario
        System.out.println("LazyComponent: constructor"); // LazyComponent: constructor
    }

    @PostConstruct
    void init() {
        // La fase init también se retrasará hasta el momento de creación del componente
        System.out.println("LazyComponent: @PostConstruct"); // LazyComponent: @PostConstruct
    }
}

Con un inicio normal del contexto, este componente no se creará hasta que alguien lo solicite (directa o indirectamente). Esto suele ser útil para objetos "auxiliares" que realmente no siempre son necesarios.

Advertencia sobre el grafo de dependencias

Aquí hay una trampa en la que caen incluso los no principiantes. Usted coloca @Lazy en un bean, inicia la aplicación, y aún así se crea al inicio. Mira la pantalla como a un traidor que le ha fallado. En realidad, Spring simplemente es honesto: si su eager singleton depende directamente de un lazy-bean, entonces al crear el eager singleton el contenedor necesita proporcionar esa dependencia, y el lazy-bean se vuelve "necesario ahora mismo".

Para ver esto, creemos un par de clases pequeñas:

class ExpensiveHelper {
    ExpensiveHelper() {
        // Simulación de un objeto auxiliar "costoso" de crear
        System.out.println("ExpensiveHelper created"); // ExpensiveHelper created
    }
}

class OrderService {
    OrderService(ExpensiveHelper helper) {
        // helper es necesario para el servicio ya en la etapa de creación (inyección por constructor)
        System.out.println("OrderService created"); // OrderService created
    }
}

Y la configuración:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
class GraphConfig {

    @Bean
    OrderService orderService(ExpensiveHelper helper) {
        // Importante: mientras creamos OrderService, el contenedor debe proporcionar helper
        return new OrderService(helper);
    }

    @Bean
    @Lazy // Pero esta anotación no salvará si helper es necesario para un servicio eager ahora mismo
    ExpensiveHelper expensiveHelper() {
        return new ExpensiveHelper();
    }
}

Si OrderService se crea eager (por defecto), entonces al iniciar el contexto requerirá ExpensiveHelper, y el contenedor creará ExpensiveHelper, incluso si está marcado con @Lazy, porque "lazy" significa "crea cuando sea necesario", y ya es necesario.

La moraleja práctica es simple: @Lazy funciona de manera predecible, pero solo si se tiene en cuenta el grafo de dependencias. En nuestro proyecto ContextFlow esto será especialmente notable en los catálogos auxiliares y gestores que "cuelgan" como dependencias de los servicios.

4. Fail-fast y fail-late

Ahora llegamos a la parte más "ingenieril". Eager y lazy no se tratan solo de la velocidad de inicio. Se trata de cuándo se entera de que algo está mal. Y esto está directamente relacionado con dónde se trasladan sus errores: al inicio de la aplicación o al "primer escenario real".

En la práctica hay dos enfoques vitales. Primero: "que la aplicación falle de inmediato y no finja que está lista". Segundo: "que inicie más rápido, y las cosas complejas se levanten según sea necesario". Spring permite elegir. Pero no hace esta elección "gratis".

Error en la fase init

Creemos un bean de demostración que "se rompe" en @PostConstruct. Por ejemplo, espera que algún flag esté activado, pero está desactivado.

import jakarta.annotation.PostConstruct;

class FragileBean {

    @PostConstruct
    void init() {
        // Rompemos artificialmente la fase init para mostrar fail-fast/fail-late
        throw new IllegalStateException("Config is invalid"); // Config is invalid
    }
}

Versión eager:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class EagerFailConfig {

    @Bean // Eager por defecto: la creación ocurrirá al iniciar el contexto
    FragileBean fragileBean() {
        return new FragileBean();
    }
}

Si simplemente crea el contexto:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class EagerFailApp {
    public static void main(String[] args) {
        try (var ctx = new AnnotationConfigApplicationContext(EagerFailConfig.class)) {
            // No llegaremos a esta línea: el contexto fallará al iniciar
            System.out.println("This line will NOT print");
        }
    }
}

No llegará a System.out.println(...): el contexto fallará al iniciar. Esto es fail-fast.

Y ahora la versión lazy:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
class LazyFailConfig {

    @Bean
    @Lazy // Creación pospuesta hasta la primera solicitud del bean
    FragileBean fragileBean() {
        return new FragileBean();
    }
}

Y el main:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class LazyFailApp {
    public static void main(String[] args) {
        try (var ctx = new AnnotationConfigApplicationContext(LazyFailConfig.class)) {
            // El contexto inicia con éxito porque FragileBean aún no se ha creado
            System.out.println("Context started"); // Context started

            // La primera solicitud a FragileBean iniciará la fase init y "explotará" aquí
            ctx.getBean(FragileBean.class);        // boom
        }
    }
}

Aquí el contexto inicia, pero la "bomba" se traslada al momento de la primera solicitud getBean(...). El error no ha desaparecido, simplemente se ha vuelto "más tarde".

Y aquí suele surgir la pregunta: "¿Qué es mejor?" No hay una respuesta universal, pero para el núcleo de la aplicación (el grafo de objetos principal) suele ser más útil conocer los problemas antes. Especialmente en un proyecto educativo, donde nos importa la previsibilidad: si se inició, significa que todo está ensamblado.

Error en el wiring y momento de detección

Otra ilusión típica: "Si hago la dependencia lazy, el contenedor dejará de quejarse de que no se encuentra el bean". A veces puede parecer que @Lazy "curó" NoSuchBeanDefinitionException. En realidad, solo ha cambiado el momento en que esto se vuelve notable.

Si la dependencia falta y es obligatoria, entonces o el contexto falla al iniciar con eager, o falla más tarde en la primera solicitud (si ha logrado una resolución diferida). Desde el punto de vista del pensamiento operativo, esto no es una "corrección", sino un traslado del problema en el tiempo. En un proyecto real, ese traslado puede estar justificado, pero solo si entiende claramente lo que hace y realmente desea "levantar parte de la aplicación bajo demanda".

Aquí es muy útil tener en mente la regla de los días anteriores: @Lazy es una herramienta. No debe reemplazar un diseño normal del grafo de dependencias ni convertirse en un "parche de anotaciones para todo".

5. Inicio rápido y costo de la primera llamada

Cuando la gente escucha por primera vez sobre lazy, a menudo piensa en el rendimiento al estilo "la aplicación inicia más rápido, por lo tanto es bueno". Pero en la vida de ingeniería hay una aburrida ley de conservación del dolor: si no hizo algo al inicio, lo más probable es que lo haga más tarde, cuando el usuario ya lo necesita.

Es decir, lazy a menudo convierte un "inicio lento" en una "primera llamada lenta". A veces es un excelente compromiso, a veces es una mala UX incluso en una aplicación de consola (como la nuestra). Por lo tanto, veamos de manera muy concreta: dónde ahorramos y dónde pagamos.

Mini medición de tiempo

Creemos un bean que "se inicializa lentamente". No conectaremos ningún perfilador, simplemente pondremos Thread.sleep(...) como simulación de una preparación pesada. (En la realidad podría ser lectura de archivos, construcción de índices, preparación de directorios, verificación de permisos, etc.)

import jakarta.annotation.PostConstruct;

class SlowBean {

    @PostConstruct
    void init() {
        // Simulamos una preparación pesada en la fase init
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("SlowBean init interrupted", e);
        }
        System.out.println("SlowBean ready"); // SlowBean ready
    }
}

Configuración eager:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class EagerSlowConfig {

    @Bean // Eager por defecto: SlowBean se creará al iniciar el contexto
    SlowBean slowBean() {
        return new SlowBean();
    }
}

Y la medición:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class EagerSlowApp {
    public static void main(String[] args) {
        // Medimos el tiempo total de inicio del contexto
        long start = System.currentTimeMillis();

        try (var ctx = new AnnotationConfigApplicationContext(EagerSlowConfig.class)) {
            long ms = System.currentTimeMillis() - start;
            System.out.println("Context started in " + ms + " ms"); // ~300+ ms
        }
    }
}

Configuración lazy:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
class LazySlowConfig {

    @Bean
    @Lazy // Posponemos la creación hasta la primera solicitud
    SlowBean slowBean() {
        return new SlowBean();
    }
}

Y la medición:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class LazySlowApp {
    public static void main(String[] args) {
        // Aquí medimos por separado el inicio del contexto y la creación del bean
        long start = System.currentTimeMillis();

        try (var ctx = new AnnotationConfigApplicationContext(LazySlowConfig.class)) {
            System.out.println("Context started"); // Context started

            // Medimos el "costo" de la primera utilización del bean
            long before = System.currentTimeMillis();
            ctx.getBean(SlowBean.class);
            long beanMs = System.currentTimeMillis() - before;

            System.out.println("SlowBean created in " + beanMs + " ms"); // ~300+ ms
        }
    }
}

El sentido será: el contexto inicia más rápido, pero paga con tiempo en getBean(...).

Tabla de compromisos

Para no convertir la elección entre eager y lazy en una guerra religiosa de "quién es más perezoso", es útil fijar el compromiso directamente en la mente. Lo más cómodo es con una tabla, porque al cerebro le resulta más fácil comparar.

Característica Eager (por defecto) Lazy (@Lazy)
Momento de creación del bean al iniciar el contexto en la primera solicitud/uso real
Momento de ejecución de la fase init al iniciar en la primera utilización
Diagnóstico de wiring/inicialización fail-fast (más temprano) fail-late (más tarde)
Tiempo de inicio del contexto generalmente mayor generalmente menor
Retardo de la primera utilización generalmente menor puede ser notablemente mayor
Predictibilidad del comportamiento mayor menor (el error "aparece" más tarde)

Para una aplicación educativa y para la mayoría de los servicios "principales", un buen default sigue siendo eager, porque hace que el sistema sea más fácil de entender: si se inició, significa que todo está ensamblado y las fases init se ejecutaron. Lazy lo dejamos como herramienta para casos particulares, cuando el bean es realmente pesado y realmente no siempre es necesario.

6. Eager y lazy en ContextFlow

Ahora fijemos este tema en nuestro dominio, pero sin detalles innecesarios: nos importa la elección entre fail-fast y "lo crearé después si es necesario". En ContextFlow tenemos un "núcleo" — servicios de escenarios de procesamiento de pedidos e infraestructura básica (almacenamiento, auditoría, envío de notificaciones), y tenemos "cosas auxiliares" que pueden no ser necesarias siempre.

Si tiene en mente el modelo "el núcleo debe estar listo al inicio", entonces casi automáticamente llega a esta idea: los servicios principales de la aplicación (por ejemplo, el hipotético OrderPlacementService) es mejor crearlos eager, para que los errores de wiring y configuración se detecten de inmediato. En cambio, los catálogos de plantillas o gestores de salida que se "activan" solo en algunos escenarios, a veces se pueden hacer lazy, pero solo si está dispuesto a aceptar el costo de la primera utilización.

Por ejemplo, imagine que NotificationTemplateCatalog prepara un mapa interno de plantillas, y ReportOutputManager prepara el directorio para la salida de informes. Si inicia la aplicación solo para crear un pedido y enviar una notificación por consola, quizás el informe no sea necesario en absoluto. En ese caso, la infraestructura de informes podría ser candidata a lazy. Pero si el informe es necesario casi siempre, entonces lazy no proporcionará una ventaja: simplemente trasladará el trabajo a la primera llamada, y el sentido de "aceleración" desaparecerá.

Una regla intermedia que funciona bien para un principiante: si no está seguro, haga eager. Eager es más fácil de depurar y más fácil de explicarse a sí mismo. Active lazy solo cuando pueda responder honestamente dos preguntas. Primera: "¿por qué este bean realmente no es necesario al inicio?". Segunda: "¿qué pasará si se rompe no al inicio, sino después de cinco minutos de funcionamiento?". Para el catálogo de plantillas y el output-manager, esto generalmente significa una elección bastante aburrida pero útil: eager por defecto.

7. Errores típicos con @Lazy

Error n.º 1: poner @Lazy "en todas partes para acelerar" y luego sorprenderse con caídas extrañas "en un momento aleatorio".
Esta decisión convierte rápidamente la aplicación en una caja de sorpresas: el inicio es bonito, y luego, de repente, la primera operación de trabajo falla debido a un problema que eager habría detectado de inmediato. Si desea un inicio más rápido, primero asegúrese de que realmente hay beans "pesados y no siempre necesarios", y que está preparado para el fail-late.

Error n.º 2: pensar que @Lazy elimina el error, cuando simplemente lo traslada en el tiempo.
Si en la fase init lanza una excepción o el wiring no se puede ensamblar, lazy solo hace que el problema se manifieste no al iniciar el contexto, sino en el momento de la primera utilización. Esto es útil como herramienta para gestionar el momento de inicialización, pero es malo como "medicina para un stack trace rojo".

Error n.º 3: esperar que @Lazy en un bean funcione incluso si un bean eager depende directamente de él.
Si su servicio principal se crea eager y requiere una dependencia a través del constructor, el contenedor debe proporcionar esa dependencia ahora mismo. Por lo tanto, un lazy-bean puede muy bien crearse al inicio "en contra" de las expectativas. Esto no es un bug de Spring, es una consecuencia natural del grafo de dependencias.

Error n.º 4: olvidar que junto con la creación del bean también se pospone su fase init (@PostConstruct, initMethod, afterPropertiesSet).
A veces el desarrollador pone @Lazy pensando que "solo el objeto se creará más tarde", y luego se sorprende de que la preparación de la caché, la verificación de la configuración o la impresión de diagnóstico también se hayan trasladado. Es importante recordar: el ciclo de vida se inicia por completo cuando el bean se crea realmente.

Error n.º 5: usar lazy como una forma de ocultar una mala arquitectura de dependencias.
Si añade @Lazy porque de lo contrario hay un ciclo de dependencias o "no se ensambla", la mayoría de las veces no resuelve el problema, sino que lo hace menos visible al inicio. Es mucho más útil preguntarse primero si las responsabilidades no están mezcladas y si no es necesario extraer un componente separado, en lugar de tratar el grafo con anotaciones.

1
Tarea
Spring Core, nivel 10, lección 3
Bloqueada
Eager singleton se crea al inicio
Eager singleton se crea al inicio
1
Tarea
Spring Core, nivel 10, lección 3
Bloqueada
Lazy bean se crea en la primera solicitud
Lazy bean se crea en la primera solicitud
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION