A logo showing the text blog.marcnuri.com
English
Inicio»Java»Spring Bean Scopes: Singleton con Prototypes

Entradas Recientes

  • Fabric8 Kubernetes Client 7.2.0 está disponible!
  • Conectarse a un servidor MCP con JavaScript y AI SDK
  • Conectarse a un servidor MCP con JavaScript y LangChain.js
  • El Futuro de las Herramientas para Desarrolladores en la era de la IA
  • Conectarse a un servidor Model Context Protocol (MCP) con Java y LangChain4j

Categorías

  • Antiguo
  • Front-end
  • Go
  • Herramientas
  • Industria y negocios
  • Inteligencia Artificial
  • Java
  • JavaScript
  • Operaciones
  • Personal
  • Proyectos personales

Archivos

  • mayo 2025
  • abril 2025
  • marzo 2025
  • febrero 2025
  • enero 2025
  • diciembre 2024
  • noviembre 2024
  • agosto 2024
  • junio 2024
  • mayo 2024
  • abril 2024
  • marzo 2024
  • febrero 2024
  • enero 2024
  • diciembre 2023
  • noviembre 2023
  • octubre 2023
  • septiembre 2023
  • agosto 2023
  • julio 2023
  • junio 2023
  • mayo 2023
  • abril 2023
  • marzo 2023
  • febrero 2023
  • enero 2023
  • diciembre 2022
  • noviembre 2022
  • octubre 2022
  • agosto 2022
  • julio 2022
  • mayo 2022
  • marzo 2022
  • febrero 2022
  • enero 2022
  • diciembre 2021
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • diciembre 2020
  • octubre 2020
  • agosto 2020
  • junio 2020
  • mayo 2020
  • marzo 2020
  • febrero 2020
  • enero 2020
  • noviembre 2019
  • octubre 2019
  • julio 2019
  • diciembre 2018
  • agosto 2018
  • julio 2018
  • junio 2018
  • mayo 2018
  • marzo 2018
  • febrero 2018
  • noviembre 2017
  • octubre 2017
  • agosto 2017
  • julio 2017
  • enero 2017
  • julio 2016
  • enero 2016
  • diciembre 2015
  • noviembre 2015
  • diciembre 2014
  • marzo 2014
  • febrero 2011
  • junio 2008
  • mayo 2008
  • abril 2008
  • enero 2008
  • junio 2007
  • mayo 2007
  • abril 2007
  • marzo 2007

Spring Bean Scopes: Singleton con Prototypes

2018-06-06 en Java etiquetado Bean / Java / Ámbitos / SOLID / Spring Framework / Spring Boot / Testing por Marc Nuri | Última actualización: 2021-04-01
English version

Introducción

Esta es la segunda publicación en la seria acerca de Spring Bean Scopes. En el tutorial anterior vimos que cuando se inyectaban Beans de ámbito Prototype dentro de un Singleton aparecían algunas trabas. El principal problema es que las dependencias Autowired se inyectan en el Singleton cuando este se instancia (evento que ocurre una sola vez) por lo que aunque el Bean inyectado se haya declarado como Prototype, en realidad se comportará como un Singleton.

El siguiente código resalta este comportamiento:

1@Test
2public void getAutowiredSample_na_shouldBeSameInstance() {
3  // Given
4  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7  // When
8  Sample autowiredSample1 = singleton1.getAutowiredSample();
9  Sample autowiredSample2 = singleton2.getAutowiredSample();
10
11  // Then
12  assertEquals(singleton1, singleton2);
13  assertEquals(autowiredSample1, autowiredSample2);
14  assertEquals(autowiredSample1.getUuid(), autowiredSample2.getUuid());
15}

En el ejemplo anterior, a pesar de que el objeto devuelto por el método getAutowiredSample se ha declarado como un Bean de ámbito Prototype, la instancia devuelta en las dos peticiones es la misma.

Para resolver este problema disponemos de diversas alternativas.

Anotación @Lookup

La anotación @Lookup nos permite anotar métodos tipo getter para un Bean específico. El contenedor de Spring se encargará de anular este método en tiempo de ejecución y redirigirlo al BeanFactory para realizar una llamada estándar al método getBean, devolviendo el Bean que este método devolvería (Singleton, Prototype, Request, Session…). En este caso, al haber definido el Bean con ámito Prototype, para cada petición se devolverá una nueva instancia del Bean.

El siguiente fragmento de código muestra como emplear la anotación @Lookup:

1@Lookup
2public Sample getSampleUsingLookup() {
3  return null;
4}

El principal problema con esta alternativa es que sólo funcionara con Beans de ámbito Singleton que se hayan definido empleando anotaciones de tipo @Component y que se hayan instanciado mediante escaneo de componentes (@ComponentScan) o autodetección. No funcionará con Beans que se hayan definido con anotaciones @Bean.

El siguiente test muestra el comportamiento de la anotación @Lookup en un singleton definido con una anotación @Component:

1@Test
2public void getSampleUsingLookupInComponent_na_shouldBeDifferent() {
3  // Given
4  SingletonWithLookupPrototype singleton1 = applicationContext.getBean(SingletonWithLookupPrototype.class);
5  SingletonWithLookupPrototype singleton2 = applicationContext.getBean(SingletonWithLookupPrototype.class);
6
7  // When
8  Sample lookedupSample1 = singleton1.getSampleUsingLookup();
9  Sample lookedupSample2 = singleton2.getSampleUsingLookup();
10
11  // Then
12  assertEquals(singleton1, singleton2);
13  assertNotNull(singleton1);
14  assertNotNull(singleton2);
15  assertNotEquals(lookedupSample1, lookedupSample2);
16  assertNotEquals(lookedupSample1.getUuid(), lookedupSample2.getUuid());
17}

Tal como se muestra en el fragmento anterior, este es el comportamiento que estamos esperando. Hay dos objetos creados a partir de un Singleton Bean que en realidad son la misma instancia a pesar de haberse pedido dos veces (comportamiento de los Singleton). Cuando se hacen peticiones al método getSampleUsingLookup() se devuelven instancias diferentes de la clase Sample ya que se ha definido como un Bean de ámbito Prototype.

Tal como ya se ha comentado, hay un problema con esta técnica y es que sólo funcionará con Beans de ámbito Singleton que se hayan definido con una anotación de tipo @Component. Si el Bean se declara utilizando un procedimiento como el siguiente:

1@Bean
2@Scope(SCOPE_SINGLETON)
3public ClassWithSamplePrototypes singletonWithSamplePrototypes(Sample autowiredSample, ObjectFactory<Sample> sampleObjectFactory) {
4  return new ClassWithSamplePrototypes(autowiredSample, sampleObjectFactory);
5}

El test fallará ya que el método getSampleUsingLoopup() no se anulará por el contenedor de Spring y por lo tanto devolverá siempre null. Podemos comprobar este comportamiento con el test anterior aplicando algunas modificaciones:

1@Test
2public void getSampleUsingLookupInRegularBean_na_shouldBeNull() {
3  // Given
4  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7  // When
8  Sample lookedupSample1 = singleton1.getSampleUsingLookup();
9  Sample lookedupSample2 = singleton2.getSampleUsingLookup();
10
11  // Then
12  assertEquals(singleton1, singleton2);
13  assertNull(lookedupSample1);
14  assertNull(lookedupSample2);
15}

Utilizando ApplicationContext

Otro sistema para sobreponerse al problema de la inyección de beans con ámbitos es inyectar ApplicationContext en el el Bean de ámbito Singleton. Siempre que se necesite una instancia del Bean de ámbito Prototype, el Singleton hará una petición a la instancia de ApplicationContext.

El procedimiento para el Singleton sería el siguiente:

1/** ... **/
2public class ClassWithSamplePrototypes implements ApplicationContextAware {
3/** ... **/
4  private ApplicationContext applicationContext;
5
6  @Override
7  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
8    this.applicationContext = applicationContext;
9  }
10/** ... **/
11  public Sample getSampleUsingApplicationContext() {
12    return applicationContext.getBean(Sample.class);
13  }
14/** ... **/
15}

Podemos verificar el comportamiento esperado con el siguiente test:

1@Test
2public void getSampleUsingApplicationContext_na_shouldBeDifferent() {
3  // Given
4  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7  // When
8  Sample applicationContextSample1 = singleton1.getSampleUsingApplicationContext();
9  Sample applicationContextSample2 = singleton2.getSampleUsingApplicationContext();
10
11  // Then
12  assertEquals(singleton1, singleton2);
13  assertNotNull(singleton1);
14  assertNotNull(singleton2);
15  assertNotEquals(applicationContextSample1, applicationContextSample2);
16  assertNotEquals(applicationContextSample1.getUuid(), applicationContextSample2.getUuid());
17}

Aunque esta técnica es efectiva, claramente vulnera los principios de SOLID al contradecir el principio de la inversión de control. Las dependencias se solicitan directamente al contenedor de Spring en lugar de inyectarse en la clase por lo que el código queda completamente acoplado a Spring (e.g. no hay forma de lanzar el test anterior sin emplear SpringJUnit4ClassRunner ).

Interfaz ObjectFactory de Spring

Por último, aunque en mi opinión, la forma más limpia de resolver el problema del Bean inyectado con ámbito es utilizar la interfaz ObjectFactory de Spring. Mediante este procedimiento, en lugar de inyectar el Bean en sí mismo, lo que hacemos es inyectar una factoría para el Bean empleando esta interfaz.

1/** ... **/
2public class ClassWithSamplePrototypes implements ApplicationContextAware {
3/** ... **/
4  private final ObjectFactory<Sample> sampleObjectFactory;
5  @Autowired
6  public ClassWithSamplePrototypes(Sample autowiredSample, ObjectFactory<Sample> sampleObjectFactory) {
7    this.autowiredSample = autowiredSample;
8    this.sampleObjectFactory = sampleObjectFactory;
9  }
10/** ... **/
11  public Sample getSampleUsingObjectFactory() {
12    return sampleObjectFactory.getObject();
13  }
14/** ... **/
15}

En lugar de que el Bean se inyecte durante la instanciación de la clase con ámbito Singleton, se inyecta una instancia de la interfaz ObjectFactory para un tipo concreto de Bean. Dependiendo del tipo y ámbito configurado para ObjectFactory, su método getObject devolverá una instancia del Bean de acuerdo con dicha configuración.

El siguiente test muestra el comportamiento esperado:

1@Test
2public void getSampleUsingObjectFactory_na_shouldBeDifferent() {
3  // Given
4  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7  // When
8  Sample objectFactorySample1 = singleton1.getSampleUsingObjectFactory();
9  Sample objectFactorySample2 = singleton2.getSampleUsingObjectFactory();
10
11  // Then
12  assertEquals(singleton1, singleton2);
13  assertNotNull(singleton1);
14  assertNotNull(singleton2);
15  assertNotEquals(objectFactorySample1, objectFactorySample2);
16  assertNotEquals(objectFactorySample1.getUuid(), objectFactorySample2.getUuid());
17}

El empleo de ObjectFactory no violará ninguno de los principios de la programación orientada a objetos ya que siempre se puede inyectar una implementación específica de ObjectFactory sin depender de Spring ni ningún otro contenedor:

1new ClassWithSamplePrototypes(new Sample(),
2    () -> new Sample());

Conclusión

Esta publicación muestra como resolver el problema de los Beans inyectados con ámbito que ocurre cuando se inyecta un Bean con ámbito Prototype dentro de un Bean de ámbito Singleton. Hemos visto las diferentes técnicas para resolver dicho problema y las ventajas y desventajas de cada una de ellas.

El código fuente completo de este artículo puede encontrarse en GitHub.

Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Navegador de artículos
Linux: Cómo listar paquetes RPM instaladosSpring Boot: ¿Cómo cambiar el puerto de la aplicación?
© 2007 - 2025 Marc Nuri