Spring Bean Scopes: Guía para comprender los distintos scopes (ámbitos) de un Spring Bean
Introducción
En este tutorial se muestran los distintos ámbitos (Scopes) para un Bean que podemos encontrar en Spring Framework.
Es prioritario entender que la definición de un Bean no es más que una receta para crear instancias de una clase de acuerdo con dicha definición. Partiendo de esta base, se puede asumir que la misma receta para crear instancias de una clase se puede emplear en múltiples ocasiones o varias veces durante el ciclo de vida de una aplicación.
Una de las características que permite Spring configurar en la definición de los Beans es el Scope o ámbito de los objetos creados a partir de dicha definición.
Spring por defecto incluye 7 Scopes diferentes:
- Singleton
- Prototype
- Request
- Session
- Global session
- Application
- Websocket
De ellos, los 5 últimos sólo están disponibles en una aplicación web-aware.
Singleton
Singleton es el ámbito por defecto de un Bean. Este scope implica que el contenedor de Spring creará una única instancia compartida de la clase designada por este Bean, por lo que siempre que se solicite este Bean se estará inyectando el mismo objeto. La instancia se almacenará en un caché gestionado por Spring.
Podemos definir los singletons empleando anotaciones del siguiente modo:
1@Bean(name = SINGLETON_BEAN_SAMPLE_NAME)
2public Sample sample() {
3 return new Sample();
4}
5
6@Bean(name = SINGLETON_ANNOTATED_BEAN_SAMPLE_NAME)
7@Scope(SCOPE_SINGLETON)
8public Sample sampleAnnotated() {
9 return new Sample();
10}
Como se ha mencionado, Singleton es el ámbito por defecto, por lo que no es necesario definir su @Scope
(primer ejemplo). No obstante, y para mayor claridad se puede emplear la anotación @Scope
para subrayar este hecho.
El siguiente test muestra el comportamiento de los singletons:
1@Test
2public void singletonTest_na_shouldBeSameInstance() {
3 Sample singleton1 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_NAME, Sample.class);
4 Sample singleton2 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_NAME, Sample.class);
5 Assert.assertEquals(singleton1, singleton2);
6 Sample singletonAnnotated1 = applicationContext.getBean(SINGLETON_ANNOTATED_BEAN_SAMPLE_NAME, Sample.class);
7 Sample singletonAnnotated2 = applicationContext.getBean(SINGLETON_ANNOTATED_BEAN_SAMPLE_NAME, Sample.class);
8 Assert.assertEquals(singletonAnnotated1, singletonAnnotated2);
9 Assert.assertNotEquals(singleton1, singletonAnnotated1);
10}
Del mismo modo que para los beans regulares, cualquier clase anotada con @Component
o cualquiera de sus extensiones se comportará como un Singleton a menos que se indique lo contrario.
Así, para un @RestController
como el siguiente:
1@RestController
2public class SingletonScopedController extends AbstractController {
3 /* ... */
4 @GetMapping(AbstractController.SINGLETON_SCOPE_ENDPOINT)
5 public String getUuid() {
6 return super.getUuid();
7 }
8}
Podemos hacer un test como el siguiente:
1@Test
2public void singletonScopedController_na_shouldReturnSameValues() throws Exception {
3 String response1 = mockMvc.perform(get(AbstractController.SINGLETON_SCOPE_ENDPOINT))
4 .andReturn().getResponse().getContentAsString();
5 String response2 = mockMvc.perform(get(AbstractController.SINGLETON_SCOPE_ENDPOINT))
6 .andReturn().getResponse().getContentAsString();
7 Assert.assertEquals(response1, response2);
8}
Prototype
El ámbito Prototype implica que el contenedor de Spring creará una nueva instancia del objeto descrito por el Bean cada vez que se le haga una petición. Una de las reglas básicas que indica la documentación de Spring es que para entornos donde haya un mantenimiento de sesión (Stateful) se emplee el Prototype Scope y cuando no se mantenga la sesión del usuario (Stateless) se emplee el Singleton Scope, no obstante puede haber más casos de uso.
Es importante destacar que Spring no se “responsabiliza” de lo que ocurra con la instancia del objeto recibida. Es decir, el desarrollador es responsable de realizar las tareas de limpieza y liberación de recursos referenciados por el objeto definido por el Bean.
Del mismo modo que para los Singleton, podemos definir un Prototype con anotaciones del siguiente modo:
1@Bean(name = PROTOTYPE_BEAN_SAMPLE_NAME)
2@Scope(SCOPE_PROTOTYPE)
3public Sample samplePrototype() {
4 return new Sample();
5}
El siguiente test muestra el comportamiento del prototype:
1@Test
2public void prototypeTest_na_shouldBeDifferentInstance() {
3 Sample prototype1 = applicationContext.getBean(PROTOTYPE_BEAN_SAMPLE_NAME, Sample.class);
4 Sample prototype2 = applicationContext.getBean(PROTOTYPE_BEAN_SAMPLE_NAME, Sample.class);
5 Assert.assertNotEquals(prototype1, prototype2);
6}
Singleton Beans con Prototype Beans inyectados
Es muy importante resaltar que cuando se define un Bean de tipo Singleton que contiene dependencias a Prototype Beans, la inyección ocurre cuando el objeto singleton se instancia, por lo que la inyección ocurre una sola vez y por tanto el objeto inyectado por el Prototype Bean será siempre el mismo (a pesar de ser de ámbito Prototype).
Por ejemplo, para una clase con un Bean inyectado como la siguiente:
1public class SampleAutowired {
2
3 @Autowired
4 @Qualifier(BEAN_NAME_FOR_AUTOWIRED_PROTOTYPE)
5 private Sample sampleAutowiredPrototype;
6
7 /* ... */
8}
Podemos definir los siguientes Beans:
1@Bean(name = PROTOTYPE_BEAN_SAMPLE_NAME)
2@Scope(SCOPE_PROTOTYPE)
3@Qualifier(BEAN_NAME_FOR_AUTOWIRED_PROTOTYPE)
4public Sample samplePrototype() {
5 return new Sample();
6}
7
8@Bean(name = SINGLETON_BEAN_SAMPLE_AUTOWIRED_NAME)
9@Scope(SCOPE_SINGLETON)
10public SampleAutowired sampleAutowiredPrototype() {
11 return new SampleAutowired();
12}
El primer Bean (Sample
) es de ámbito Prototype y se inyectará en el segundo (SampleAutowired
) que es de ámbito Singleton. El comportamiento previsto es que siempre que recuperemos SampleAutowired
la instancia de Sample
será la misma, a pesar de ser un Prototype ya que éste se inyectó cuando se creó la instancia del Singleton.
El siguiente test demuestra esto:
1@Test
2public void singletonWithAutowiredPrototype_na_shouldBeInstance() {
3 SampleAutowired singleton1 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_AUTOWIRED_NAME, SampleAutowired.class);
4 SampleAutowired singleton2 = applicationContext.getBean(SINGLETON_BEAN_SAMPLE_AUTOWIRED_NAME, SampleAutowired.class);
5 Assert.assertEquals(singleton1, singleton2);
6 Assert.assertEquals(singleton1.getSampleAutowiredPrototype(), singleton2.getSampleAutowiredPrototype());
7}
Ámbitos de una aplicación web-aware
Tal como se ha mencionado, el resto de ámbitos sólo están disponibles en aplicaciones web-aware.
Request
El contenedor de Spring creará una nueva instancia del objeto definido por el Bean cada vez que reciba un HTTP request.
Para configurar un Bean con este ámbito podemos emplear las siguientes anotaciones en función de si se trata un Bean o un Componente:
1// Beans basados en anotaciones @Bean
2@Bean
3@Scope(value = WebApplicationContext.SCOPE_REQUEST)
4public BeanSample beanSample() {
5 return new BeanSample ();
6}
7
8// Beans basados en @Component
9@RestController
10@RequestScope
11public class RequestScopedController extends AbstractController{
12
13 @GetMapping(AbstractController.REQUEST_SCOPE_ENDPOINT)
14 public String getUuid() {
15 return super.getUuid();
16 }
17
18}
Para el @RestController
anterior podemos demostrar su comportamiento con el siguiente test:
1@Test
2public void requestScopedController_na_shouldReturnDifferentValues() throws Exception {
3 String response1 = mockMvc.perform(get(AbstractController.REQUEST_SCOPE_ENDPOINT))
4 .andReturn().getResponse().getContentAsString();
5 String response2 = mockMvc.perform(get(AbstractController.REQUEST_SCOPE_ENDPOINT))
6 .andReturn().getResponse().getContentAsString();
7 Assert.assertNotEquals(response1, response2);
8}
Session
El contenedor de Spring creará una nueva instancia del objeto definido por el Bean para cada una de las sesiones HTTP (Aplicación Stateful) y entregará esa misma instancia cada vez que reciba una petición dentro de la misma sesión.
1// Beans basados en anotaciones @Bean
2@Bean
3@Scope(value = WebApplicationContext.SCOPE_SESSION)
4public BeanSample beanSample() {
5 return new BeanSample ();
6}
7
8// Beans basados en componentes
9@RestController
10@SessionScope
11public class SessionScopedController extends AbstractController {
12
13 @GetMapping(AbstractController.SESSION_SCOPE_ENDPOINT)
14 public String getUuid() {
15 return super.getUuid();
16 }
17}
Para el @RestController
anterior podemos demostrar su comportamiento con el siguiente test:
1@Test
2public void sessionScopedController_na_shouldReturnSameValues() throws Exception {
3 MockHttpSession session1 = new MockHttpSession();
4 MockHttpSession session2 = new MockHttpSession();
5 String response1_1 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session1))
6 .andReturn().getResponse().getContentAsString();
7 String response1_2 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session1))
8 .andReturn().getResponse().getContentAsString();
9 Assert.assertEquals(response1_1, response1_2);
10 String response2_1 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session2))
11 .andReturn().getResponse().getContentAsString();
12 String response2_2 = mockMvc.perform(get(AbstractController.SESSION_SCOPE_ENDPOINT).session(session2))
13 .andReturn().getResponse().getContentAsString();
14 Assert.assertEquals(response2_1, response2_2);
15 Assert.assertNotEquals(response1_1, response2_1);
16}
Global Session
El ámbito Global Session tiene un comportamiento similar al de Session pero aplica solo en el contexto de aplicaciones web basadas en Portlets. La especificación de los Portlets define un tipo de sesión global que se comparte entre todos los portlets que componen una aplicación.
Si empleásemos este ámbito en una aplicación web normal, el comportamiento de los Beans sería el mismo que si se hubiesen definido con un scope de tipo Session.
1@Bean
2@Scope(value = WebApplicationContext.SCOPE_GLOBAL_SESSION)
3public BeanSample beanSample() {
4 return new BeanSample ();
5}
Application
El ámbito Application tiene un comportamiento similar al de Singleton ya que Spring creará una nueva instancia del Bean definido para cada ServletContext
. Sin embargo, a diferencia del Singleton Scope, una aplicación de Spring puede contar con varios ServletContext con lo que la instancia que se devuelva para cada petición al Bean será la misma dentro de un contexto, pero diferente para cada uno de los ServletContext.
Websocket
El contenedor de Spring creará una nueva instancia del objeto definido por el Bean y la devolverá para cada petición que se produzca dentro del ciclo de vida de un Websocket.
Conclusión
Este post sirve como guía rápida para la comprensión de los distintos Scopes o ámbitos que existen dentro de una aplicación de Spring. Se incluyen ejemplos prácticos con JUnit tests donde se demuestran los diferentes comportamientos.
Todo el código que se muestra en la publicación está disponible en Github.
Comentarios en "Spring Bean Scopes: Guía para comprender los distintos scopes (ámbitos) de un Spring Bean"