Spring Bean Scopes: Guía para comprender los distintos scopes de un Spring Bean
Introducción
En Spring Framework, la definición de un bean es esencialmente una receta para crear instancias de objetos. El scope (ámbito) determina cuándo y cómo Spring instancia estos objetos: ¿debería crear una instancia para toda la aplicación, o una nueva para cada petición?
Spring proporciona seis scopes para beans:
| Scope | Duración | Caso de uso |
|---|---|---|
| Singleton | Aplicación | Servicios stateless, recursos compartidos |
| Prototype | Por petición al contenedor | Objetos stateful, builders |
| Request | Petición HTTP | Datos específicos de la petición |
| Session | Sesión HTTP | Datos de sesión de usuario |
| Application | ServletContext | Estado compartido a nivel de app |
| WebSocket | Sesión WebSocket | Datos específicos de WebSocket |
Los últimos cuatro (Request, Session, Application, WebSocket) requieren un ApplicationContext web-aware y no funcionarán en aplicaciones standalone.
Scope Singleton
Singleton es el scope por defecto. Spring crea exactamente una instancia del bean por contenedor y la cachea para todas las peticiones posteriores. Cada vez que solicitas este bean, obtienes el mismo objeto.
Puedes definir singletons usando anotaciones:
@Bean(name = SINGLETON_BEAN_SAMPLE_NAME)
public Sample sample() {
return new Sample();
}
@Bean(name = SINGLETON_ANNOTATED_BEAN_SAMPLE_NAME)
@Scope(SCOPE_SINGLETON)
public Sample sampleAnnotated() {
return new Sample();
}Como Singleton es el valor por defecto, la anotación @Scope es opcional (primer ejemplo).
Recomiendo añadirla explícitamente (segundo ejemplo) para que tu intención quede clara para otros desarrolladores.
El siguiente test demuestra el comportamiento singleton:
@Test
public void singletonTest_na_shouldBeSameInstance() {
Sample singleton1 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_NAME, Sample.class);
Sample singleton2 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_NAME, Sample.class);
Assert.assertEquals(singleton1, singleton2);
Sample singletonAnnotated1 = applicationContext.getBean(SINGLETON_ANNOTATED_BEAN_SAMPLE_NAME, Sample.class);
Sample singletonAnnotated2 = applicationContext.getBean(SINGLETON_ANNOTATED_BEAN_SAMPLE_NAME, Sample.class);
Assert.assertEquals(singletonAnnotated1, singletonAnnotated2);
Assert.assertNotEquals(singleton1, singletonAnnotated1);
}Las clases anotadas con @Component (o sus especializaciones como @Service, @Repository, @Controller) siguen las mismas reglas: singleton por defecto.
Aquí un ejemplo con @RestController:
@RestController
public class SingletonScopedController extends AbstractController {
/* ... */
@GetMapping(AbstractController.SINGLETON_SCOPE_ENDPOINT)
public String getUuid() {
return super.getUuid();
}
}Este test usando MockMvc verifica el comportamiento singleton:
@Test
public void singletonScopedController_na_shouldReturnSameValues() throws Exception {
String response1 = mockMvc.perform(get(AbstractController.SINGLETON_SCOPE_ENDPOINT))
.andReturn().getResponse().getContentAsString();
String response2 = mockMvc.perform(get(AbstractController.SINGLETON_SCOPE_ENDPOINT))
.andReturn().getResponse().getContentAsString();
Assert.assertEquals(response1, response2);
}Scope Prototype
Con el scope Prototype, Spring crea una nueva instancia cada vez que solicitas el bean al contenedor. Esto es útil para objetos stateful que no deberían compartirse, como builders u objetos con estado mutable.
Advertencia
Spring no gestiona el ciclo de vida completo de los beans prototype. Una vez que el contenedor te entrega la instancia, tú eres responsable de la limpieza y liberación de recursos.
Define un bean prototype usando anotaciones:
@Bean(name = PROTOTYPE_BEAN_SAMPLE_NAME)
@Scope(SCOPE_PROTOTYPE)
public Sample samplePrototype() {
return new Sample();
}Este test muestra que cada petición devuelve una instancia diferente:
@Test
public void prototypeTest_na_shouldBeDifferentInstance() {
Sample prototype1 = applicationContext.getBean(PROTOTYPE_BEAN_SAMPLE_NAME, Sample.class);
Sample prototype2 = applicationContext.getBean(PROTOTYPE_BEAN_SAMPLE_NAME, Sample.class);
Assert.assertNotEquals(prototype1, prototype2);
}El problema del prototype en singleton
Un error común: cuando inyectas un bean Prototype en un Singleton, la inyección ocurre solo una vez (cuando se crea el Singleton). Esto significa que tu "prototype" efectivamente se comporta como un singleton.
Considera esta clase con una dependencia inyectada:
public class SampleAutowired {
@Autowired
@Qualifier(BEAN_NAME_FOR_AUTOWIRED_PROTOTYPE)
private Sample sampleAutowiredPrototype;
/* ... */
}Con estas definiciones de beans:
@Bean(name = PROTOTYPE_BEAN_SAMPLE_NAME)
@Scope(SCOPE_PROTOTYPE)
@Qualifier(BEAN_NAME_FOR_AUTOWIRED_PROTOTYPE)
public Sample samplePrototype() {
return new Sample();
}
@Bean(name = SINGLETON_BEAN_SAMPLE_AUTOWIRED_NAME)
@Scope(SCOPE_SINGLETON)
public SampleAutowired sampleAutowiredPrototype() {
return new SampleAutowired();
}El bean Sample tiene scope Prototype, pero como SampleAutowired es Singleton, Spring inyecta Sample solo una vez durante la instanciación.
Cada petición posterior de SampleAutowired devuelve el mismo objeto con la misma instancia de Sample, anulando el propósito del scope Prototype.
El ejemplo PrototypeInSingleton demuestra esto:
@Test
public void singletonWithAutowiredPrototype_na_shouldBeInstance() {
SampleAutowired singleton1 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_AUTOWIRED_NAME, SampleAutowired.class);
SampleAutowired singleton2 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_AUTOWIRED_NAME, SampleAutowired.class);
Assert.assertEquals(singleton1, singleton2);
Assert.assertEquals(singleton1.getSampleAutowiredPrototype(), singleton2.getSampleAutowiredPrototype());
}Consejo
Para soluciones a este problema usando ObjectProvider, @Lookup o scoped proxies, consulta mi publicación complementaria: Spring Bean Scopes: Singleton con Prototypes.
Scopes web-aware
Los scopes restantes (Request, Session, Application, WebSocket) solo funcionan en aplicaciones web con un WebApplicationContext.
Scope Request
Spring crea una nueva instancia para cada petición HTTP. El bean vive solo durante esa petición y se destruye cuando se envía la respuesta.
Define un bean con scope request:
// Beans basados en anotaciones @Bean
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public BeanSample beanSample() {
return new BeanSample ();
}
// Beans basados en @Component
@RestController
@RequestScope
public class RequestScopedController extends AbstractController{
@GetMapping(AbstractController.REQUEST_SCOPE_ENDPOINT)
public String getUuid() {
return super.getUuid();
}
}Cada petición obtiene una instancia nueva:
@Test
public void requestScopedController_na_shouldReturnDifferentValues() throws Exception {
String response1 = mockMvc.perform(get(AbstractController.REQUEST_SCOPE_ENDPOINT))
.andReturn().getResponse().getContentAsString();
String response2 = mockMvc.perform(get(AbstractController.REQUEST_SCOPE_ENDPOINT))
.andReturn().getResponse().getContentAsString();
Assert.assertNotEquals(response1, response2);
}Scope Session
Spring crea una instancia por sesión HTTP. Todas las peticiones dentro de la misma sesión comparten la misma instancia del bean, ideal para datos específicos del usuario como carritos de compra o preferencias.
// Beans basados en anotaciones @Bean
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION)
public BeanSample beanSample() {
return new BeanSample ();
}
// Beans basados en componentes
@RestController
@SessionScope
public class SessionScopedController extends AbstractController {
@GetMapping(AbstractController.SESSION_SCOPE_ENDPOINT)
public String getUuid() {
return super.getUuid();
}
}Este test muestra que las peticiones de la misma sesión comparten instancia, pero sesiones diferentes obtienen instancias diferentes:
@Test
public void sessionScopedController_na_shouldReturnSameValues() throws Exception {
MockHttpSession session1 = new MockHttpSession();
MockHttpSession session2 = new MockHttpSession();
String response1_1 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session1))
.andReturn().getResponse().getContentAsString();
String response1_2 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session1))
.andReturn().getResponse().getContentAsString();
Assert.assertEquals(response1_1, response1_2);
String response2_1 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session2))
.andReturn().getResponse().getContentAsString();
String response2_2 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session2))
.andReturn().getResponse().getContentAsString();
Assert.assertEquals(response2_1, response2_2);
Assert.assertNotEquals(response1_1, response2_1);
}Scope Application
El scope Application crea una instancia por ServletContext.
Esto es similar a Singleton, pero la distinción importa cuando múltiples aplicaciones basadas en servlets comparten el mismo ServletContext: compartirán la misma instancia del bean.
Para la mayoría de aplicaciones Spring Boot con un único ServletContext, el scope Application se comporta idénticamente a Singleton.
Scope WebSocket
Spring crea una instancia por sesión WebSocket. El bean vive durante la conexión WebSocket y se comparte entre todos los mensajes de esa sesión.
Scope Thread
Spring proporciona SimpleThreadScope para instancias de beans por hilo, pero no está registrado por defecto.
Regístralo manualmente si lo necesitas:
@Configuration
public class ThreadScopeConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new SimpleThreadScope());
return configurer;
}
}Nota
Spring 5.0 eliminó el scope Global Session que se usaba para aplicaciones basadas en Portlets. Si trabajas con código legacy de Portlet, este scope ya no está disponible.
Preguntas frecuentes
¿Cuál es el scope por defecto en Spring?
Singleton. Si no especificas un scope, Spring crea una instancia por contenedor y la reutiliza para todas las peticiones.
¿Cuándo debería usar Prototype en lugar de Singleton?
Usa Prototype cuando cada consumidor necesita su propia instancia con estado independiente. Ejemplos comunes incluyen builders, objetos command, o cualquier bean que mantenga estado mutable que no debería compartirse.
¿Puedo inyectar un bean Prototype en un Singleton?
Sí, pero con un detalle: el Prototype se inyecta una vez cuando se crea el Singleton.
Para un comportamiento Prototype real, usa ObjectProvider, @Lookup o scoped proxies.
Consulta Spring Bean Scopes: Singleton con Prototypes para soluciones detalladas.
¿Cuál es la diferencia entre Singleton y el scope Application?
En la mayoría de apps Spring Boot, son idénticos.
La diferencia aparece cuando múltiples aplicaciones basadas en servlets comparten el mismo ServletContext: los beans con scope Application se comparten entre todas ellas, mientras que los beans Singleton están limitados a un único contexto de aplicación.
Conclusión
Los scopes de beans de Spring controlan cuándo y cómo se instancian los objetos. El scope correcto depende de tu caso de uso:
| Scope | Usa cuando... |
|---|---|
| Singleton | El bean es stateless o se puede compartir de forma segura (la mayoría de servicios) |
| Prototype | Cada consumidor necesita estado independiente |
| Request | Los datos son específicos de una petición HTTP |
| Session | Los datos persisten entre peticiones del usuario (carrito, preferencias) |
| Application | Se comparte entre múltiples servlets en el mismo contexto |
| WebSocket | Los datos persisten durante una conexión WebSocket |
Para más información sobre patrones de inyección de dependencias y testing de componentes Spring, consulta mis otros tutoriales.
El código fuente completo con ejemplos ejecutables con JBang está disponible en GitHub.
Referencias


Comentarios en "Spring Bean Scopes: Guía para comprender los distintos scopes de un Spring Bean"