
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 agetBean.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:
- Instanciación: se llama al constructor.
- Inyección de dependencias: se completan las propiedades y parámetros de constructor anotados.
- Callbacks "aware": si el bean implementa interfaces como
BeanNameAwareoApplicationContextAware, Spring inyecta esa información antes de la inicialización. - Post-procesamiento (antes de la inicialización):
BeanPostProcessor.postProcessBeforeInitialization. - Inicialización, en este orden: el método anotado con
@PostConstruct, luegoafterPropertiesSet()(si el bean implementaInitializingBean), luego elinit-methodpersonalizado, si se declaró. - Post-procesamiento (después de la inicialización):
BeanPostProcessor.postProcessAfterInitialization. Aquí, por ejemplo, es donde suelen crearse los proxies de AOP. - Bean listo para usarse: disponible en el contenedor hasta que se cierre el contexto.
- Destrucción, al cerrar el contexto:
@PreDestroy, luegodestroy()(si implementaDisposableBean), luego eldestroy-methodpersonalizado.
@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.