lang://
Archivo

002 · 2026-07-24

Spring Boot: el contenedor, los beans y su ciclo de vida

Índice

Quien empieza con Spring suele aprender las anotaciones antes de entender qué pasa realmente detrás de ellas. Esta es la introducción que me hubiera gustado leer primero: qué es el contenedor, qué es un bean y cómo Spring gestiona su ciclo de vida, con un poco más de profundidad de la que suele tener la introducción típica.

El contenedor de IoC

En esencia, Spring es un contenedor de Inversión de Control (IoC). En vez de que tu código cree y conecte objetos manualmente con new, describes los componentes de la aplicación y dejas que el contenedor los instancie, los configure y los conecte entre sí.

La interfaz central es ApplicationContext. Lee la configuración (anotaciones, clases @Configuration o XML, hoy en día casi siempre las dos primeras), construye el grafo de dependencias y mantiene los objetos gestionados durante todo el ciclo de vida de la aplicación.

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);

En la práctica, con Spring Boot casi nunca instancias el contexto manualmente. Quien hace eso por detrás, al ejecutar SpringApplication.run(...), es el propio @SpringBootApplication.

Antes de que exista cualquier bean

Antes de que el contenedor instancie el primer bean "normal" de la aplicación, ya procesó toda la configuración a través de un BeanFactoryPostProcessor. A diferencia del BeanPostProcessor (que actúa sobre instancias ya creadas, como vas a ver más abajo en la sección del ciclo de vida), el BeanFactoryPostProcessor actúa sobre las definiciones de bean, la metadata que describe cómo debería construirse cada bean, antes de que se instancie ningún bean de verdad.

El ejemplo más común es PropertySourcesPlaceholderConfigurer, encargado de resolver placeholders como ${jdbc.url} en las definiciones de bean, reemplazándolos por los valores que vienen de application.properties o de otras fuentes de propiedades. Cuando usas @Value("${mi.propiedad}"), es este mecanismo el que trabaja por debajo.

@Configuration
public class AppConfig {
 
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholder() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Fíjate en el static del método @Bean. Es necesario porque un BeanFactoryPostProcessor debe instanciarse muy temprano, antes de que el contenedor procese las anotaciones @Configuration de forma normal. Declararlo como método estático evita que toda la clase de configuración termine inicializándose antes de tiempo.

Qué es un bean

Un bean es cualquier objeto cuyo ciclo de vida es gestionado por el contenedor. Las formas más comunes de declarar uno son:

@Component
public class UserService {
    // Spring lo detecta vía component scan y lo registra como bean
}
@Configuration
public class AppConfig {
 
    @Bean
    public UserService userService(UserRepository repository) {
        return new UserService(repository);
    }
}

@Service, @Repository y @Controller son especializaciones de @Component, semánticamente distintas, pero técnicamente idénticas para el contenedor.

Alcance (scope) de los beans

Por defecto, todo bean es singleton: una única instancia por contenedor, compartida entre todos los puntos que la inyectan. Los scopes más comunes son:

  • singleton (por defecto): una instancia por contenedor.
  • prototype: una instancia nueva en cada inyección o llamada a getBean.
  • request / session: scope web, una instancia por petición o sesión HTTP.
@Component
@Scope("prototype")
public class ReportBuilder {
    // una instancia nueva cada vez que se inyecta
}

Un problema común aparece cuando un bean singleton depende de un bean request o session. Como el singleton se crea una sola vez, no puede simplemente inyectar una instancia de esos scopes más pequeños, que cambian en cada petición. La solución de Spring es el proxyMode: en vez de inyectar el bean real, el contenedor inyecta un proxy que resuelve la instancia correcta del scope menor cada vez que se llama a un método.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CarritoDeCompras {
    // el singleton que inyecta este bean recibe un proxy,
    // no la instancia real de la petición actual
}

El ciclo de vida de un bean

Este es el punto que más suele confundir. El contenedor no solo crea el bean y se olvida de él, hay una secuencia de pasos bien definida:

  1. Instanciación: se llama al constructor.
  2. Inyección de dependencias: se completan las propiedades y parámetros de constructor anotados.
  3. Callbacks "aware": si el bean implementa interfaces como BeanNameAware o ApplicationContextAware, Spring inyecta esa información antes de la inicialización.
  4. Post-procesamiento (antes de la inicialización): BeanPostProcessor.postProcessBeforeInitialization.
  5. Inicialización, en este orden: el método anotado con @PostConstruct, luego afterPropertiesSet() (si el bean implementa InitializingBean), luego el init-method personalizado, si se declaró.
  6. Post-procesamiento (después de la inicialización): BeanPostProcessor.postProcessAfterInitialization. Aquí, por ejemplo, es donde suelen crearse los proxies de AOP.
  7. Bean listo para usarse: disponible en el contenedor hasta que se cierre el contexto.
  8. Destrucción, al cerrar el contexto: @PreDestroy, luego destroy() (si implementa DisposableBean), luego el destroy-method personalizado.
@Component
public class ConexionExterna implements InitializingBean, DisposableBean {
 
    @PostConstruct
    public void alIniciar() {
        System.out.println("1. @PostConstruct");
    }
 
    @Override
    public void afterPropertiesSet() {
        System.out.println("2. afterPropertiesSet");
    }
 
    @PreDestroy
    public void antesDeCerrar() {
        System.out.println("3. @PreDestroy");
    }
 
    @Override
    public void destroy() {
        System.out.println("4. destroy()");
    }
}

Un detalle poco conocido fuera de la documentación oficial: los callbacks de inicialización, incluido @PostConstruct, se ejecutan dentro del lock de creación del singleton, y el bean solo se considera totalmente listo después de que ese callback retorna. En la práctica, esto significa que buscar otro bean que también está en creación desde dentro de un @PostConstruct puede bloquear el arranque de toda la aplicación. Si un bean necesita esperar a que otros beans estén completamente listos antes de ejecutar alguna lógica, el camino más seguro es escuchar el evento ApplicationReadyEvent en vez de hacerlo dentro del propio @PostConstruct.

En la práctica, el día a día usa casi siempre solo @PostConstruct y @PreDestroy. Las interfaces InitializingBean/DisposableBean acoplan tu código a Spring y son más útiles para quien escribe librerías que para aplicaciones finales.

Orden entre beans: depends-on y SmartLifecycle

Todo esto cubre el ciclo de vida de un bean aislado, pero una aplicación real tiene decenas o cientos de ellos, y el orden entre ellos también importa. Por defecto, Spring decide ese orden a partir del grafo de dependencias: si el bean A depende del bean B, B se crea e inicializa antes que A, y se destruye después de él. Cuando no hay una dependencia directa pero el orden sigue importando, se puede forzar con @DependsOn:

@Component
@DependsOn("conexionExterna")
public class ServicioDeMigracion {
    // garantiza que "conexionExterna" ya se inicializó antes que este bean
}

Para beans que necesitan un start()/stop() explícito, como un listener de cola o un scheduler, Spring ofrece la interfaz SmartLifecycle. Añade el concepto de fase, vía getPhase(): las fases menores arrancan primero y se detienen al final, las fases mayores arrancan al final y se detienen primero. Así es como un listener de mensajería puede arrancar solo después de que la infraestructura de base de datos esté lista, y detenerse antes que ella durante el shutdown.

Por qué importa

Entender este ciclo evita bugs sutiles: dependencias que aún no se inyectaron cuando corre el constructor, conexiones que se filtran porque el cleanup nunca se implementó, beans prototype usados como si fueran singleton, o un @PostConstruct que bloquea el arranque de la aplicación al intentar acceder a otro bean que todavía se está creando. Cuando un bean se comporta de una forma que no coincide con lo esperado, casi siempre se puede señalar un paso concreto de este ciclo como causa, en vez de tratarlo como magia desconocida del framework.