Spring Framework
Spring: ciclo de vida de los beans, scopes y configuración avanzada
Intermedio9 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Saber cuándo el contenedor crea, inicializa y destruye un bean explica la mayoría de los «¿por qué esto es null?» y «¿por qué esto se ejecuta dos veces?». Esta guía recorre el ciclo de vida fase por fase, compara los scopes singleton, prototype y web junto con sus proxies, y explica la maquinaria de configuración —@Import, @Profile, @Value, @PropertySource, @Conditional, @Lazy— sobre la que Spring Boot construye su autoconfiguración.
Lo que aprenderás
- Seguir un bean desde su definición hasta su destrucción y situar cada callback en la fase correcta
- Elegir el scope adecuado y saber cuándo hace falta un scoped proxy
- Inyectar un prototype en un singleton sin congelar una única instancia
- Dividir la configuración con @Import, @Profile y @PropertySource, y leer valores con @Value y Environment
- Escribir un BeanPostProcessor y explicar cómo Spring usa ese mismo mecanismo para sus proxies
La inyección de dependencias te dice qué recibe un bean. El ciclo de vida te dice cuándo, y el scope te dice cuántos. Casi todo el comportamiento confuso de Spring —un campo inyectado que es null dentro de un constructor, un método de inicialización que se ejecuta dos veces, un bean con scope request que falla al arrancar, un prototype que nunca cambia— es en el fondo una cuestión de ciclo de vida o de scope. Esta guía se centra en Spring Framework 6.x y da por hecho que ya sabes declarar beans con configuración Java.
El ciclo de vida, fase por fase
La vida de un bean se divide claramente en fases del contenedor y fases de la instancia.
Fase de definición
Cuando un ApplicationContext se refresca, primero recopila las definiciones de beans: objetos de metadatos con la clase, el scope, los argumentos del constructor, los valores de las propiedades, los nombres de los métodos de inicialización y destrucción y el indicador lazy. Todavía no se ha instanciado nada. En este momento se ejecutan las implementaciones de BeanFactoryPostProcessor, que pueden añadir, eliminar o modificar definiciones. El mecanismo de placeholders que resuelve ${db.url} en la configuración es uno de ellos; los import selectors de la autoconfiguración de Spring Boot son otro.
Instanciación e inyección de propiedades
Para cada singleton no lazy, en un orden que respeta las dependencias, el contenedor:
- Instancia el objeto llamando al constructor elegido, resolviendo primero cada uno de sus argumentos. Si el constructor de
AnecesitaB,Bse crea por completo antes de que se ejecute el constructor deA. - Rellena los puntos de inyección por setter y por campo (
@Autowired,@Value,@Resource). - Llama a las interfaces Aware si están implementadas:
BeanNameAware.setBeanName,BeanFactoryAware,ApplicationContextAware,EnvironmentAware,ResourceLoaderAware. Estas le dan al bean acceso a las tripas del contenedor; son útiles en código de framework y una mala señal en código de negocio.
Inicialización
- Cada
BeanPostProcessorregistrado recibepostProcessBeforeInitialization(bean, name). Aquí vive el processor que gestiona@PostConstruct. - Se ejecutan los métodos
@PostConstruct, luegoInitializingBean.afterPropertiesSet()y después el método indicado en@Bean(initMethod = "…"). - Cada
BeanPostProcessorrecibepostProcessAfterInitialization(bean, name). Aquí es donde ocurre el autoproxy de AOP: el processor puede devolver un objeto distinto —un proxy—, y es ese proxy, no tu instancia, lo que entra en el registro de singletons y se inyecta en otros sitios.
El bean ya está en uso. Los beans SmartLifecycle reciben además start() una vez que todo el contexto se ha refrescado, que es el punto correcto para lógica del tipo «empezar a consumir mensajes», que necesita que todos los demás beans estén listos.
Destrucción
Cuando se llama a close() sobre el contexto (o salta el shutdown hook de la JVM registrado con registerShutdownHook()), los singletons se destruyen en orden inverso de dependencias: @PreDestroy, luego DisposableBean.destroy() y después el método @Bean(destroyMethod). Para los métodos @Bean, Spring infiere automáticamente un método de destrucción llamado close o shutdown, así que un bean HikariDataSource se cierra sin configurar nada.
@Component
public class IndexWarmer implements SmartLifecycle {
private final SearchIndex index;
private volatile boolean running;
public IndexWarmer(SearchIndex index) { this.index = index; }
@PostConstruct
void checkConfig() { // injection is complete; safe to use `index`
index.validate();
}
@Override public void start() { index.warm(); running = true; } // whole context is ready
@Override public void stop() { running = false; }
@Override public boolean isRunning() { return running; }
@PreDestroy
void release() { index.close(); }
}
La regla que resuelve la mayoría de los bugs de ciclo de vida: con inyección por campo o por setter, los campos inyectados no están disponibles en el constructor. Sí lo están en @PostConstruct. Con inyección por constructor la pregunta desaparece, lo que es un argumento más a su favor.
Scopes
Un scope define cuántas instancias de una definición de bean existen y cuánto vive cada una.
| Scope | Instancias | Duración | Disponible en |
|---|---|---|---|
singleton (por defecto) | Una por contenedor | Todo el contexto | En todas partes |
prototype | Una por inyección o por getBean | Hasta que deja de estar referenciada; sin callback de destrucción | En todas partes |
request | Una por petición HTTP | La petición | Contextos web |
session | Una por sesión HTTP | La sesión | Contextos web |
application | Una por ServletContext | La aplicación web | Contextos web |
websocket | Una por sesión WebSocket | La sesión | Aplicaciones WebSocket |
Aquí singleton significa uno por contenedor, no uno por JVM: dos contextos tienen dos instancias, y una clase puede registrarse dos veces con nombres distintos. Como los singletons se comparten entre threads, deben ser stateless o thread-safe. Guardar datos de una petición en un campo de un singleton es un bug clásico y grave.
El scope prototype y la trampa del singleton
Declarar @Scope("prototype") hace que el contenedor cree una instancia nueva cada vez que se pide el bean. La trampa es que un singleton lo pide exactamente una vez —al crearse él mismo— y a partir de ahí conserva esa instancia para siempre. Hay tres salidas:
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ReportBuilder { /* holds per-report mutable state */ }
@Service
public class ReportService {
private final ObjectProvider<ReportBuilder> builders;
public ReportService(ObjectProvider<ReportBuilder> builders) { this.builders = builders; }
public Report build(Query q) {
ReportBuilder builder = builders.getObject(); // new instance each call
return builder.with(q).build();
}
}
ObjectProvider es la más limpia. @Lookup sobre un método abstracto hace que Spring genere una subclase cuyo método devuelve un bean nuevo. Un scoped proxy (ver más abajo) también funciona, pero está pensado para los scopes web.
Scopes web y scoped proxies
Un bean con scope request no se puede inyectar directamente en un singleton: al arrancar no hay ninguna petición, así que no hay nada que inyectar, y el contenedor falla con un mensaje que indica que no hay ninguna petición asociada al thread. La solución es inyectar un proxy que resuelva el objeto real de scope request en cada llamada a un método:
@Component
@RequestScope // shorthand for @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestAudit {
private final List<String> events = new ArrayList<>();
public void record(String e) { events.add(e); }
public List<String> events() { return events; }
}
@Service
public class CheckoutService {
private final RequestAudit audit; // actually a CGLIB proxy
public CheckoutService(RequestAudit audit) { this.audit = audit; }
public void checkout() { audit.record("checkout"); }
}
@RequestScope, @SessionScope y @ApplicationScope configuran el modo de proxy por ti. El proxy es una subclase del objetivo (TARGET_CLASS) o una implementación de interfaz (INTERFACES); en ambos casos la clase tiene que poder proxificarse: no puede ser final y debe tener un constructor accesible. Los beans con scope session deberían ser Serializable si las sesiones pueden persistirse o replicarse.
Funcionalidades de configuración que dan forma al contexto
Dividir e importar
Las aplicaciones reales reparten la configuración en varias clases según su responsabilidad: DataConfig, WebConfig, SecurityConfig. Se unen con @Import, que es explícito y funciona fuera de los paquetes escaneados:
@Configuration
@Import({DataConfig.class, MessagingConfig.class})
public class AppConfig { }
@Import también acepta un ImportSelector —una clase que decide en tiempo de ejecución qué configuraciones importar—, que es el mecanismo detrás de todas las anotaciones @Enable… y de la autoconfiguración de Spring Boot.
@Configuration(proxyBeanMethods = false) desactiva la generación de subclases con CGLIB descrita en la guía de inyección de dependencias. Las llamadas entre métodos @Bean pasan entonces a crear objetos nuevos en lugar de devolver el gestionado, así que tienes que pasar las dependencias como parámetros. Las propias autoconfiguraciones de Spring Boot usan este modo «lite» para arrancar más rápido, y es una opción por defecto razonable para tu código una vez que adoptas el estilo con parámetros.
Perfiles
Un perfil es una porción de configuración con nombre que se activa según el entorno. Cualquier @Component, @Configuration o @Bean puede llevar @Profile:
@Configuration
public class DataConfig {
@Bean
@Profile("local")
DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
@Bean
@Profile("!local") // expressions: !, &, |
DataSource pooledDataSource(Environment env) {
var ds = new HikariDataSource();
ds.setJdbcUrl(env.getRequiredProperty("db.url"));
return ds;
}
}
Activa los perfiles con la propiedad de sistema o la variable de entorno spring.profiles.active, o por código con context.getEnvironment().setActiveProfiles("local") antes de refrescar el contexto. Si no hay ningún perfil activo, se registran los beans del perfil default. Los perfiles son para entornos; no los uses para activar o desactivar funcionalidades: para eso, la herramienta adecuada es una propiedad junto con @Conditional.
Propiedades, Environment y @Value
La abstracción Environment unifica las fuentes de propiedades —propiedades de sistema, variables de entorno, ficheros de propiedades y cualquier cosa que añadas— en un único orden de búsqueda. @PropertySource añade un fichero:
@Configuration
@PropertySource("classpath:app.properties")
public class AppConfig {
@Bean
Mailer mailer(@Value("${mail.host}") String host,
@Value("${mail.port:587}") int port, // default after the colon
@Value("#{systemProperties['user.name']}") String user) { // SpEL
return new SmtpMailer(host, port, user);
}
}
@Value maneja valores simples con conversión de tipos. Para configuración estructurada —un grupo de ajustes relacionados, con validación—, @ConfigurationProperties de Spring Boot es muy superior; en código de Framework puro, inyecta Environment y lee un prefijo, o haz el binding a mano. Nunca escribas secretos directamente en ficheros de propiedades que acaban en el repositorio; léelos del entorno.
Condiciones e inicialización lazy
@Conditional(SomeCondition.class) registra un bean solo cuando el método matches de la condición devuelve true. Escribir condiciones directamente es poco habitual en código de aplicación, pero entenderlas desmitifica el @ConditionalOnMissingBean de Boot: es el mismo mecanismo con una condición ya escrita.
@Lazy aplaza la creación de un singleton hasta su primer uso. Es legítimo para un bean pesado que la mayoría de las peticiones nunca tocan, y como forma de romper una referencia circular rebelde inyectando un proxy lazy. No es una estrategia de rendimiento: traslada el coste del arranque a la primera petición desafortunada y oculta los errores de configuración hasta entonces.
@DependsOn("flywayMigration") fuerza un orden de creación donde no hay relación de inyección; el caso típico es «ejecutar la migración del esquema antes de que se inicialice la entity manager factory».
Extender el contenedor
Un BeanPostProcessor ve cada bean después de su instanciación y puede envolverlo o modificarlo. El soporte de Spring para @Transactional, @Cacheable, @Async y AOP se basa por completo en post-processors que devuelven un proxy desde postProcessAfterInitialization. Escribir uno pequeño tú mismo hace tangible el mecanismo:
@Component
public class TimingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (!bean.getClass().isAnnotationPresent(Timed.class)) {
return bean; // leave everything else untouched
}
var factory = new ProxyFactory(bean); // spring-aop
factory.addAdvice((MethodInterceptor) invocation -> {
long start = System.nanoTime();
try {
return invocation.proceed();
} finally {
System.out.printf("%s took %d µs%n", invocation.getMethod().getName(),
(System.nanoTime() - start) / 1_000);
}
});
return factory.getProxy();
}
}
De aquí se derivan dos consecuencias para quien use proxies: el objeto inyectado en otros beans es el proxy, así que bean.getClass() devuelve una clase generada, y las llamadas que un bean hace a sus propios métodos se saltan el proxy por completo. Ambos puntos se tratan a fondo en la guía de AOP.
Un FactoryBean<T> es el otro punto de extensión clásico: un bean cuya tarea es producir otro bean de tipo T, y que se usa cuando la construcción es demasiado compleja para un método @Bean; LocalContainerEntityManagerFactoryBean y las factorías de repositorios de Spring Data son ejemplos. Pedir al contenedor el nombre del bean devuelve el producto; anteponer & al nombre devuelve la propia factoría.
Errores comunes
Usar un campo inyectado en el constructor. Con inyección por campo, ahí el campo todavía es null. Mueve la lógica a @PostConstruct o, mejor aún, pásate a la inyección por constructor.
Singletons con estado. Un @Service con un campo private List<Order> current está compartido por todos los threads que atienden peticiones. Mantén el estado en el ámbito del método, en un bean con scope request o en una estructura explícita y thread-safe.
Inyectar un bean con scope request sin proxy. El arranque falla porque no existe ninguna petición. Usa @RequestScope o configura proxyMode.
Esperar callbacks de destrucción en los prototypes. Nunca se disparan. Cierra tú los recursos.
Registrar un BeanPostProcessor que depende de beans normales. Los post-processors se crean antes que todo lo demás, así que los beans que arrastran se instancian antes de tiempo y se quedan sin pasar por otros post-processors; cuando ocurre, Spring registra en el log «is not eligible for getting processed by all BeanPostProcessors». Mantén los post-processors sin dependencias o usa ObjectProvider dentro de ellos.
Poner @Profile en un método de una configuración lite y olvidarse de la alternativa. Si todos los candidatos para un tipo están condicionados por perfil y ninguno coincide, la inyección falla al arrancar. Ten siempre un valor por defecto incondicional o con !profile, o haz que la dependencia sea opcional.
Conclusiones clave
- Un bean pasa por la definición, la instanciación, la inyección de propiedades, los callbacks Aware, los hooks de los post-processors, los callbacks de inicialización y, por último, la destrucción;
@PostConstructse ejecuta después de la inyección, nunca antes. postProcessAfterInitializationpuede sustituir un bean por un proxy: así es como se enganchan las transacciones, la caché y AOP.- Singleton es una instancia por contenedor compartida entre threads; prototype es una por cada petición al contenedor, sin callback de destrucción; los scopes web necesitan un scoped proxy para inyectarse en singletons.
- Inyecta
ObjectProvider<T>cuando un singleton necesite un prototype nuevo en cada llamada. @Importcompone la configuración,@Profileselecciona por entorno,@PropertySourcey@Valueleen ajustes, y@Conditionales el interruptor general sobre el que se construye la autoconfiguración de Spring Boot.@Lazyy@DependsOnson soluciones puntuales, no valores por defecto; que el arranque falle rápido es una virtud.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Qué se ejecuta primero, @PostConstruct o afterPropertiesSet?
- @PostConstruct, luego InitializingBean.afterPropertiesSet y después el @Bean(initMethod) o el init-method de XML. Los tres se ejecutan cuando la inyección de dependencias ha terminado y antes de que el bean se entregue a otros beans, así que cualquiera de ellos puede usar con seguridad los campos inyectados. Usa @PostConstruct salvo que tengas un motivo para acoplar la clase a las interfaces de Spring.
- ¿Por qué mi bean prototype es siempre la misma instancia?
- Porque se inyectó una sola vez en un singleton, y los singletons se crean una sola vez. El contenedor no tiene ocasión de crear un prototype nuevo en cada llamada. Inyecta ObjectProvider<T> y llama a getObject() cuando necesites una instancia nueva, o declara el prototype con un scoped proxy para que cada llamada a un método pase por un proxy que resuelve un objetivo nuevo.
- ¿Llama Spring a @PreDestroy en los beans prototype?
- No. El contenedor crea el prototype, lo inyecta, ejecuta sus callbacks de inicialización y se olvida de él; nunca hace seguimiento de la instancia, así que no puede destruirla. Si un prototype retiene recursos, la responsabilidad de liberarlos es del cliente que lo obtuvo, o bien lo registras en un bean post-processor propio que lleve la cuenta de las instancias.
- ¿Qué diferencia hay entre @Profile y @Conditional?
- @Profile es una condición concreta e integrada: el bean solo se registra cuando alguno de los perfiles indicados está activo en el Environment. @Conditional es el mecanismo general: tú aportas una clase Condition que puede inspeccionar cualquier cosa (una propiedad, una clase en el classpath, un bean existente), y los @ConditionalOnMissingBean, @ConditionalOnProperty y compañía de Spring Boot están todos construidos sobre él.