CodeGym /Cursos /Spring Boot /@PostConstruct y @PreDestroy

@PostConstruct y @PreDestroy

Spring Boot
Nivel 6, Lección 3
Disponible

1. El papel de @PostConstruct y @PreDestroy

Si alguna vez ha montado muebles de una caja (especialmente de esa donde las instrucciones son una hoja A4 con pictogramas misteriosos), sabrá que primero hay que montar las piezas, luego comprobar que todo se sostiene, y antes de mudarse — desmontar/empaquetar sin olvidar nada. Con los beans pasa algo similar: Spring crea el objeto, inyecta las dependencias, y a veces necesitamos hacer un pequeño «ajuste adicional» después de que todo esté conectado. Y al detenerlo — liberar cuidadosamente los recursos que el bean mantiene.

@PostConstruct y @PreDestroy son dos anotaciones de Jakarta (históricamente — JSR-250) que Spring respeta e invoca en los momentos adecuados:

  • @PostConstruct se invoca después de la creación del bean y de la inyección de dependencias (es decir, «el objeto ya está ensamblado por el contenedor»).
  • @PreDestroy se invoca antes de la destrucción del bean, cuando ApplicationContext se cierra (es decir, «es hora de ordenar y apagar la luz»).

Es importante captar la escala correcta: son callbacks locales, se refieren a un objeto (bean) específico. No son «un segundo punto de entrada a la aplicación» ni «un runner secreto que se puede esconder en cualquier clase». Si mantiene este límite en mente, evitará el estilo junior muy popular de «meterlo todo en @PostConstruct porque funciona».

2. Vida de la aplicación y vida del bean

La confusión más común aquí no es sobre la sintaxis de las anotaciones, sino sobre el significado: un novato ve que @PostConstruct se invoca «al inicio» y concluye «entonces es el lugar para toda la lógica de arranque». Luego, de repente, descubre que la aplicación arranca lentamente, la lógica está dispersa en 15 clases y nadie entiende dónde está el «verdadero inicio» y dónde solo una preparación local de campos.

Vamos a separarlo en dos escalas de tiempo.

La primera escala — el ciclo de vida de la aplicación (lo que discutimos en las lecciones sobre runners y eventos). Allí nos interesa: el contexto se levantó, la aplicación está lista, ocurrió un fallo, comienza la finalización. La segunda escala — el ciclo de vida del bean, donde nos interesa: el objeto se creó, se inyectaron las dependencias, está listo para trabajar, y luego se cierra correctamente.

Para visualizarlo, es conveniente imaginar una línea de tiempo aproximadamente así (simplificada, pero honesta para nivel junior):

flowchart TD
    A["main()"] --> B["SpringApplication.run(...)"]
    B --> C[Creación y refresh de ApplicationContext]
    C --> D[Creación de beans singleton + DI]
    D --> E["@PostConstruct de los beans"]
    E --> F[ApplicationStartedEvent]
    F --> G[ApplicationRunner / CommandLineRunner]
    G --> H[ApplicationReadyEvent]
    H --> I[La aplicación funciona]
    I --> J[Cierre del contexto]
    J --> K["@PreDestroy de los beans"]

Observe una idea importante: @PostConstruct es antes que los runners e incluso antes que «ready». Por lo tanto, si en @PostConstruct hace algo largo o «que decide el destino de la aplicación», está insertando esa acción en una fase donde Spring todavía está ensamblando el contexto activamente.

Para no elegir «por sensaciones», tenga en mente esta pequeña tabla (no es un dogma, sino una brújula práctica):

Mecanismo Nivel Cuándo se ejecuta Para qué es bueno Qué no poner ahí
Constructor objeto en el new recibir dependencias, fijar invariantes lógica de negocio de inicio, I/O, «ahora calculo todo»
@PostConstruct bean después de DI, durante la inicialización del contexto preparación local de recurso/caché, verificación ligera operaciones pesadas, llamadas de red, «inicio completo de la aplicación»
Runner (ApplicationRunner) aplicación casi al final del arranque lógica de inicio explícita de la aplicación procesos largos «de por vida» (mejor mecanismos separados)
@EventListener en lifecycle event aplicación en etapas concretas observación, diagnóstico ligero «hacer trabajo en secreto» en lugar de un inicio explícito
@PreDestroy bean al cerrar el contexto liberar recursos lógica de la que depende la corrección de la aplicación en tiempo de ejecución

3. Cuándo se invoca @PostConstruct

La propia anotación @PostConstruct suena como si la hubieran inventado personas que amaban el latín y, un poco, el sufrimiento de los novatos. En realidad es simple: «después de la construcción». Y el punto clave es después de la inyección de dependencias. Es decir, Spring ya ha creado el objeto, ya le ha pasado las dependencias a través del constructor (mantenemos este estilo), y solo entonces invoca el método @PostConstruct.

Para un novato es útil ver el orden en un ejemplo vivo, aunque sea un poco «impreso». Sí, es System.out.println. Sí, sentimos un leve dolor, pero ahora el objetivo es ver el orden. El siguiente InitOrderDemo es solo una sonda local del orden de invocación. No es necesario mantenerlo junto con todos los demás ejemplos de lifecycle solo por la colección de anotaciones.

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

@Component
public class InitOrderDemo {

    public InitOrderDemo() {
        // El constructor se invoca al crear el objeto por el contenedor Spring
        System.out.println("1) constructor"); // 1) constructor
    }

    @PostConstruct
    void init() {
        // @PostConstruct se invoca después de crear el bean e inyectar dependencias (después de DI)
        System.out.println("2) @PostConstruct"); // 2) @PostConstruct
    }
}

Ahora agreguemos un marker-runner — también temporal, solo para ver el límite entre el lifecycle del bean y el lifecycle de la aplicación:

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class RunnerMarker implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // El runner se ejecuta ya cerca del final del arranque de la aplicación, después de la inicialización de los beans
        System.out.println("3) runner"); // 3) runner
    }
}

Si ejecuta la aplicación, el orden típico de mensajes será así:

1) constructor
2) @PostConstruct
3) runner

Y aquí debería «hacer clic» en la cabeza: @PostConstruct no es «casi el final del arranque». Es el lugar donde el bean se vuelve listo «dentro del contexto», pero la aplicación aún no tiene que estar lista por completo.

Otro dato técnico importante (que se recuerda fácilmente como «regla de la firma»): el método con @PostConstruct debe ser void, sin parámetros, y no static. Es como un timbre: o suena o no; no necesita argumentos.

4. @PostConstruct vs runner

Es muy tentador convertir @PostConstruct en el «inicio de todo» universal, porque se activa de forma elegante y parece cómodo. Pero es como usar el mismo bolsillo de la mochila para las llaves, el bocadillo y el portátil: en algún momento sacará el bocadillo… con la huella del teclado. Técnicamente las cosas estaban «en el mismo sitio», pero poca alegría.

Un buen @PostConstruct es un trabajo corto y local, relacionado precisamente con ese bean. Un escenario frecuente: el bean almacena algún recurso pequeño en memoria que hay que preparar después de que todas las dependencias estén en su sitio. Si se necesita un fragmento de demostración principal sobre el bean-level lifecycle, es más conveniente mantener esa variante simétrica.

Agreguemos al catalog-service un ejemplo de componente que hace exactamente esto: registra el momento «me he inicializado» y al apagarse informa del uptime. No es «gran lógica de negocio», pero es un excelente ejemplo educativo de simetría init/cleanup.

import java.time.Duration;
import java.time.Instant;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class StartupClock {

    // Almacenamos el momento de inicio del bean para luego calcular el uptime
    private Instant startedAt;

    @PostConstruct
    void init() {
        // Inicialización después de DI: el bean ya está creado, las dependencias ya están en su sitio
        startedAt = Instant.now();
        System.out.println("StartupClock initialized"); // StartupClock initialized
    }

    @PreDestroy
    void onShutdown() {
        // Finalización simétrica: calculamos cuidadosamente e imprimimos el uptime antes de cerrar el contexto
        long seconds = Duration.between(startedAt, Instant.now()).toSeconds();
        System.out.println("Uptime seconds = " + seconds); // Uptime seconds = 12 (ejemplo)
    }
}

¿Qué hay de útil aquí, además de «mostrar el tiempo»? Hemos mostrado un hábito importante: si en un bean prepara algo, a menudo tiene un paso simétrico de «ordenar». Este pensamiento se traslada muy bien a recursos reales: descriptores de archivo, conexión a un sistema externo, cliente, pool de hilos (todavía no los creamos, pero la idea ya se puede entender).

Ahora, el límite. Si necesita hacer una acción que no se refiere a este bean, sino a la aplicación en su conjunto, entonces @PostConstruct se convierte en un mal lugar. Por ejemplo, «mostrar un resumen inicial», «verificar parámetros de entrada de inicio», «decidir si arrancar la aplicación en absoluto» — esto es claramente lógica a nivel de aplicación. Para eso ya tenemos los runners.

Sí, técnicamente puede inyectar ApplicationArguments en cualquier bean y en @PostConstruct leer los argumentos, pero obtendrá un «rompecabezas» donde las piezas del arranque están dispersas por el proyecto. El código se ejecutará «de alguna manera», pero explicarlo a otra persona será difícil.

Aquí hay un ejemplo de cómo se puede «accidentalmente» empezar a esconder lógica de inicio en el lugar equivocado. Esto es precisamente un antipatrón, no un código para snapshot permanente:

import jakarta.annotation.PostConstruct;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;

@Component
public class BadStartupLogicInsideBean {

    private final ApplicationArguments args;

    public BadStartupLogicInsideBean(ApplicationArguments args) {
        // La dependencia (args) se puede obtener, pero eso no hace de init un "buen lugar" para la lógica de inicio
        this.args = args;
    }

    @PostConstruct
    void init() {
        // Antipatrón: la lógica a nivel de aplicación comienza a vivir dentro de un bean aleatorio
        System.out.println("args = " + args.getSourceArgs().length); // args = 0 (ejemplo)
    }
}

Formalmente, funcionará. Pero muy pronto se encontrará en una situación: parte del inicio en runners, parte en @PostConstruct, parte incluso en constructores (sí, también se hace, pero eso ya es otro nivel de tristeza). Por lo tanto, la regla práctica del día es: @PostConstruct es para la preparación local del bean, runner es para la lógica de inicio explícita de la aplicación.

5. @PreDestroy: cómo «ordenar correctamente» al detenerse

Los novatos rara vez piensan en la detención de la aplicación porque «bueno, simplemente se apaga en el IDE». Y luego, de repente, comienza la vida real: la aplicación se ejecuta como un proceso, se detiene, se reinicia, y de repente resulta que «detenerse» también es una fase importante. Y si mantenía un recurso (aunque sea simple), es mejor liberarlo.

@PreDestroy es un método que Spring invoca al cerrar ApplicationContext. En Spring Boot, el contexto normalmente se cierra cuando detiene la aplicación (por ejemplo, Ctrl+C en el terminal) o cuando el proceso recibe una señal de finalización correcta. En ese momento, Spring recorre los beans y les da la oportunidad de cerrar recursos.

Aquí hay dos detalles prácticos importantes. Primero: @PreDestroy no está garantizado si el proceso se «mata de forma brusca» (como «tirar del enchufe» o hacer kill -9). Segundo: su método @PreDestroy debe ser lo más fiable y rápido posible, porque el shutdown suele ser un modo de «vamos a finalizar, no a organizar una fiesta». Si en @PreDestroy decide «ahora voy a la red y guardo un informe de 300 megas», corre el riesgo de no llegar a tiempo.

Un buen estilo es: «si abrí/creé algo en @PostConstruct, lo cierro en @PreDestroy». Incluso en el ejemplo educativo StartupClock se aprecia: preparamos startedAt, en shutdown calculamos el uptime y finalizamos.

Otro matiz importante para principiantes: @PreDestroy se refiere a aquellos beans cuyo ciclo de vida es gestionado por el contenedor. Para los beans singleton normales (que es el caso por defecto) funciona como se espera. Pero si alguna vez llega a los prototipos, la lógica de destrucción es diferente: el contenedor los crea, pero no está obligado a «clavar» su destrucción. Por ahora, basta con recordar: por defecto tiene singleton, y @PreDestroy es su gancho normal para la finalización.

6. Ejemplo para catalog-service

Para que esto no se quede en «anotaciones en el vacío», vinculemos a nuestro proyecto educativo catalog-service un componente pequeño pero real. No entramos en web, ni en configuración, ni construimos cosas de dominio complejas — nos importa practicar el patrón: preparación local + finalización simétrica.

Supongamos que queremos que la aplicación tenga un identificador de instancia (instance id) útil para diagnóstico: ve en la consola «ha arrancado tal instancia», y al finalizar «se ha detenido». Esto es algo bastante aplicado que a menudo se hace en producción (solo que se registra con logger, no con println, pero aún estamos aprendiendo). Es una variante adicional de la misma idea. Es útil como segundo ejemplo, pero no es necesario junto a StartupClock solo por la cantidad de componentes.

import java.util.UUID;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class InstanceIdHolder {

    // El identificador de instancia se mantiene en memoria durante la vida del bean
    private String instanceId;

    @PostConstruct
    void init() {
        // Generación después de DI: el bean está listo, se puede llenar de estado "local"
        instanceId = UUID.randomUUID().toString();
        System.out.println("InstanceId = " + instanceId); // InstanceId = 8f... (ejemplo)
    }

    @PreDestroy
    void onShutdown() {
        // Antes de cerrar el contexto, imprimimos un mensaje final y "ordenamos"
        System.out.println("Stopping instanceId = " + instanceId); // Stopping instanceId = 8f... (ejemplo)
    }
}

¿Por qué es un buen ejemplo educativo? Porque subraya que @PostConstruct no es «vamos a iniciar la aplicación», sino «vamos a poner este objeto concreto en estado listo». Y al mismo tiempo muestra que @PreDestroy es «vamos a finalizar correctamente», no «la última oportunidad para escribir toda la lógica de negocio».

Si ejecuta la aplicación y la detiene (por ejemplo, con Ctrl+C), verá que @PreDestroy realmente se ejecuta. Y esto es un momento psicológico importante: la finalización en Spring Boot no es «el proceso murió», sino «el contenedor dio la oportunidad de ordenar».

7. Errores típicos al trabajar con @PostConstruct y @PreDestroy

Los errores aquí generalmente no son de compilación. La aplicación arranca, las anotaciones «funcionan», y por eso el problema se vuelve traicionero: el código se comporta mal no porque Java se queje, sino porque arquitectónicamente ha elegido el lugar equivocado.

Error nº 1: usar @PostConstruct como «el lugar principal de inicio de la aplicación».
Esto comienza de forma inocente: añade una línea, luego otra, luego una «verificación rápida», luego «y vamos a cargar datos». Al final, la lógica de inicio queda dispersa por varios beans y entender el orden se vuelve difícil. Es mejor mantener @PostConstruct local, y para la lógica de inicio a nivel de aplicación usar runners, porque están diseñados precisamente para eso.

Error nº 2: esconder trabajo pesado en @PostConstruct (I/O, red, cálculos largos).
Cuando en @PostConstruct aparece Thread.sleep, lectura de archivos enormes o viajes a la red, convierte la inicialización del contexto en una «lotería». La aplicación puede tardar mucho en arrancar, o incluso caer — y esto ocurrirá en una fase donde Spring aún está ensamblando el mundo alrededor. Si el trabajo realmente pertenece al inicio de la aplicación en su conjunto, hágalo explícitamente en un runner. Si el trabajo pertenece al runtime (debe funcionar constantemente), probablemente no es tarea de @PostConstruct.

Error nº 3: esperar «el orden correcto» entre diferentes @PostConstruct en distintos beans.
Un novato a menudo piensa: «llamaré a la clase A... y se inicializará antes que B...». Spring no funciona así. El orden de creación de los beans está determinado por las dependencias y las reglas internas del contenedor, no por el alfabeto. Si un bean necesita que otro esté listo, eso debe expresarse a través de dependencias (por ejemplo, mediante el constructor), no mediante esperanzas.

Error nº 4: meter lógica en el constructor en lugar de en @PostConstruct.
El constructor en el enfoque Spring es necesario para recibir dependencias y llevar el objeto a un estado mínimamente correcto. Si comienza en el constructor a imprimir resúmenes, ir a algún sitio o hacer «inicialización del sistema», complica la vida al contenedor y aumenta la probabilidad de problemas extraños. El constructor es para dependencias. @PostConstruct es para la preparación local después de DI. Runner es para el inicio de la aplicación.

Error nº 5: pensar que @PreDestroy siempre funcionará bajo cualquier condición.
Si el proceso se finaliza de forma brusca, la JVM puede no dar tiempo a Spring para cerrar el contexto correctamente, y @PreDestroy no alcanzará. Por lo tanto, @PreDestroy no es el lugar donde se guarda «la única oportunidad de conservar datos importantes». Es bueno para liberar recursos y finalizar ordenadamente, pero no debe ser el único punto del que dependa la consistencia del negocio.

1
Tarea
Spring Boot, nivel 6, lección 3
Bloqueada
Inicialización local del bean mediante `@PostConstruct`
Inicialización local del bean mediante `@PostConstruct`
1
Tarea
Spring Boot, nivel 6, lección 3
Bloqueada
Simetría `@PostConstruct` y `@PreDestroy`
Simetría `@PostConstruct` y `@PreDestroy`
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION