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

Entradas Recientes

  • Eclipse JKube 1.19 está disponible!
  • Resumen de 2025: El Año de la IA
  • Synology DS224+: Cómo actualizar discos duros en RAID 1
  • Fabric8 Kubernetes Client 7.5 está disponible!
  • Impulsando Mi Productividad como Desarrollador con IA en 2025

Categorías

  • Antiguo
  • Cloud Native
  • Desarrollo Backend
  • Desarrollo Frontend
  • Herramientas
  • Ingeniería de calidad
  • Inteligencia Artificial
  • JavaScript
  • Operaciones
  • Personal
  • Proyectos personales
  • Reflexiones sobre Ingeniería

Archivos

  • febrero 2026
  • enero 2026
  • diciembre 2025
  • octubre 2025
  • septiembre 2025
  • julio 2025
  • 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
  • septiembre 2022
  • agosto 2022
  • julio 2022
  • junio 2022
  • mayo 2022
  • marzo 2022
  • febrero 2022
  • enero 2022
  • diciembre 2021
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • enero 2021
  • diciembre 2020
  • octubre 2020
  • septiembre 2020
  • agosto 2020
  • junio 2020
  • mayo 2020
  • marzo 2020
  • febrero 2020
  • enero 2020
  • noviembre 2019
  • septiembre 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
  • noviembre 2014
  • octubre 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 Desarrollo Backend etiquetado Bean / Java / Ámbitos / SOLID / Spring Framework / Spring Boot / Testing por Marc Nuri | Última actualización: 2026-02-16
English version

Introducción

En el tutorial anterior sobre scopes de beans, cubrí un problema común: cuando inyectas un bean Prototype en un Singleton, el prototype se comporta como singleton. ¿Por qué? Porque Spring inyecta las dependencias cuando crea el Singleton, lo cual ocurre exactamente una vez.

Este artículo presenta cuatro soluciones para obtener un comportamiento Prototype real dentro de un Singleton:

EnfoqueMejor paraCompensación
ObjectProviderLa mayoría de casosRequiere cambios de código
ObjectFactoryEscenarios simplesMenos funcionalidades
@LookupBeans escaneados por componentesNo funciona con @Bean
Scoped ProxyScope Request/SessionNueva instancia por llamada a método

Este es el código del problema:

@Test
public void getAutowiredSample_na_shouldBeSameInstance() {
  // Given
  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);

  // When
  Sample autowiredSample1 = singleton1.getAutowiredSample();
  Sample autowiredSample2 = singleton2.getAutowiredSample();

  // Then
  assertEquals(singleton1, singleton2);
  assertEquals(autowiredSample1, autowiredSample2);
  assertEquals(autowiredSample1.getUuid(), autowiredSample2.getUuid());
}

A pesar de que Sample está definido con scope Prototype, ambas peticiones devuelven la misma instancia. El prototype se inyectó una vez cuando se creó el Singleton, y esa es la instancia que siempre obtendrás.

Anotación @Lookup

La anotación @Lookup indica a Spring que sobrescriba un método en tiempo de ejecución, delegando a BeanFactory.getBean(). Cada llamada devuelve una instancia nueva cuando el bean destino tiene scope Prototype.

Este es el patrón de uso:

@Lookup
public Sample getSampleUsingLookup() {
  return null;
}

Advertencia

@Lookup solo funciona con beans descubiertos mediante escaneo de componentes (@Component y sus especializaciones). No funcionará con métodos @Bean porque Spring necesita crear una subclase del bean para sobrescribir el método.

Este test demuestra @Lookup funcionando correctamente en un bean @Component:

@Test
public void getSampleUsingLookupInComponent_na_shouldBeDifferent() {
  // Given
  SingletonWithLookupPrototype singleton1 = applicationContext.getBean(SingletonWithLookupPrototype.class);
  SingletonWithLookupPrototype singleton2 = applicationContext.getBean(SingletonWithLookupPrototype.class);

  // When
  Sample lookedupSample1 = singleton1.getSampleUsingLookup();
  Sample lookedupSample2 = singleton2.getSampleUsingLookup();

  // Then
  assertEquals(singleton1, singleton2);
  assertNotNull(singleton1);
  assertNotNull(singleton2);
  assertNotEquals(lookedupSample1, lookedupSample2);
  assertNotEquals(lookedupSample1.getUuid(), lookedupSample2.getUuid());
}

Las instancias singleton son idénticas (singleton1 == singleton2), pero cada llamada a getSampleUsingLookup() devuelve una instancia fresca de Sample.

Sin embargo, si defines tu bean usando @Bean en lugar de @Component, el método @Lookup no será sobrescrito:

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

El método @Lookup devuelve null porque Spring no creó una subclase del bean para sobrescribirlo:

@Test
public void getSampleUsingLookupInRegularBean_na_shouldBeNull() {
  // Given
  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);

  // When
  Sample lookedupSample1 = singleton1.getSampleUsingLookup();
  Sample lookedupSample2 = singleton2.getSampleUsingLookup();

  // Then
  assertEquals(singleton1, singleton2);
  assertNull(lookedupSample1);
  assertNull(lookedupSample2);
}

Utilizando ApplicationContext

Otro enfoque es inyectar ApplicationContext en el Singleton y llamar a getBean() cuando necesites una nueva instancia Prototype:

/** ... **/
public class ClassWithSamplePrototypes implements ApplicationContextAware {
/** ... **/
  private ApplicationContext applicationContext;

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }
/** ... **/
  public Sample getSampleUsingApplicationContext() {
    return applicationContext.getBean(Sample.class);
  }
/** ... **/
}

Podemos verificar que funciona con un test:

@Test
public void getSampleUsingApplicationContext_na_shouldBeDifferent() {
  // Given
  ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
  ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);

  // When
  Sample applicationContextSample1 = singleton1.getSampleUsingApplicationContext();
  Sample applicationContextSample2 = singleton2.getSampleUsingApplicationContext();

  // Then
  assertEquals(singleton1, singleton2);
  assertNotEquals(applicationContextSample1, applicationContextSample2);
  assertNotEquals(applicationContextSample1.getUuid(), applicationContextSample2.getUuid());
}

Este enfoque funciona, pero acopla tu código directamente al contenedor de Spring. Las dependencias se solicitan en lugar de inyectarse, lo que contradice la inversión de control. Para mejor testeabilidad y menor acoplamiento, considera usar ObjectProvider u ObjectFactory en su lugar.

ObjectFactory y ObjectProvider (recomendado)

La solución más limpia es inyectar una factoría en lugar del bean directamente. Spring proporciona ObjectFactory y su extensión más potente ObjectProvider.

Consejo

ObjectProvider es la opción preferida desde Spring 4.3. Añade métodos como getIfAvailable(), getIfUnique() y stream() que hacen mucho más limpio el trabajo con beans opcionales o múltiples.

ObjectFactory (enfoque básico)

ObjectFactory es una interfaz funcional simple con un único método: getObject(). Inyéctala en lugar del bean directamente, y Spring proporcionará una factoría que delega al contenedor:

public class SingletonWithObjectFactory {
  private final ObjectFactory<Sample> sampleFactory;

  public SingletonWithObjectFactory(ObjectFactory<Sample> sampleFactory) {
    this.sampleFactory = sampleFactory;
  }

  public Sample getNewSample() {
    return sampleFactory.getObject();
  }
}

Cada llamada a sampleFactory.getObject() devuelve una instancia nueva cuando el bean destino tiene scope Prototype.

ObjectProvider (enfoque preferido)

ObjectProvider extiende ObjectFactory con métodos de conveniencia adicionales:

public class SingletonWithObjectProvider {
  private final ObjectProvider<Sample> sampleProvider;

  public SingletonWithObjectProvider(ObjectProvider<Sample> sampleProvider) {
    this.sampleProvider = sampleProvider;
  }

  public Sample getNewSample() {
    return sampleProvider.getObject();
  }

  // ObjectProvider tiene métodos adicionales útiles:
  public Sample getOrDefault(Sample defaultSample) {
    return sampleProvider.getIfAvailable(() -> defaultSample);
  }
}

¿Por qué preferir ObjectProvider?

  • getIfAvailable(): Devuelve null o un valor por defecto si el bean no existe
  • getIfUnique(): Devuelve el bean solo si existe exactamente un candidato
  • stream(): Itera sobre todos los beans coincidentes
  • Soporta dependencias opcionales sin lanzar excepciones

En lugar de inyectar el bean directamente, inyectas una interfaz de factoría. Cada llamada a getObject() delega al contenedor, devolviendo una instancia fresca para beans Prototype.

Este es el test:

@Test
public void getSampleUsingObjectProvider_na_shouldBeDifferent() {
  // Given
  SingletonWithObjectProvider singleton1 = context.getBean(SingletonWithObjectProvider.class);
  SingletonWithObjectProvider singleton2 = context.getBean(SingletonWithObjectProvider.class);

  // When
  Sample sample1 = singleton1.getNewSample();
  Sample sample2 = singleton2.getNewSample();

  // Then
  assertEquals(singleton1, singleton2);
  assertNotEquals(sample1, sample2);
  assertNotEquals(sample1.getUuid(), sample2.getUuid());
}

La belleza de este enfoque es la testeabilidad. Puedes mockear fácilmente la factoría en tests unitarios sin Spring:

new SingletonWithObjectFactory(() -> new Sample());

Scoped Proxy

Con scoped proxies, Spring inyecta un proxy CGLIB en lugar del bean real. El proxy intercepta las llamadas a métodos y delega a una instancia fresca cada vez.

Esto funciona de forma transparente; la clase consumidora no necesita ningún cambio:

@Configuration
public class AppConfig {

  @Bean
  @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
  public Sample sample() {
    return new Sample();
  }

  @Bean
  public SingletonWithProxy singletonWithProxy(Sample sample) {
    return new SingletonWithProxy(sample);
  }
}

La clase singleton no necesita ningún tratamiento especial:

public class SingletonWithProxy {
  private final Sample sample;

  public SingletonWithProxy(Sample sample) {
    this.sample = sample;
  }

  public String getSampleUuid() {
    // Cada llamada al proxy crea una nueva instancia prototype
    return sample.getUuid();
  }
}

Ventajas:

  • Cero cambios de código en la clase consumidora
  • Configuración simple basada en anotaciones

Desventajas:

  • Cada llamada a método crea una nueva instancia (puede ser excesivo para Prototype)
  • Requiere CGLIB (o usa ScopedProxyMode.INTERFACES para proxies JDK)

Consejo

Los scoped proxies brillan con beans de scope Request y Session, donde el proxy resuelve transparentemente la instancia correcta para el contexto HTTP actual.

Preguntas frecuentes

¿Qué enfoque debería usar?

Empieza con ObjectProvider. Es el enfoque más flexible, testeable e idiomático. Usa scoped proxies para beans Request/Session donde quieras resolución transparente.

¿Funcionará @Lookup con métodos @Bean?

No. Spring necesita crear una subclase del bean para sobrescribir el método @Lookup, lo cual solo ocurre con beans escaneados por componentes. Para definiciones @Bean, usa ObjectProvider en su lugar.

¿Cuál es el impacto en rendimiento de estos enfoques?

Todos los enfoques añaden una sobrecarga mínima. ObjectProvider y ObjectFactory implican una llamada a getBean(). Los scoped proxies añaden intercepción de métodos vía CGLIB. En la práctica, la diferencia es insignificante a menos que estés llamando miles de veces por segundo.

¿Puedo usar estas técnicas con beans de scope Request o Session?

Sí, y los scoped proxies son particularmente útiles aquí. Resuelven automáticamente el bean correcto para la petición HTTP o sesión actual sin código de búsqueda explícito.

Conclusión

Cuando inyectas un bean Prototype en un Singleton, el comportamiento por defecto de Spring anula el propósito del scope Prototype. Aquí está el resumen:

EnfoqueCuándo usar
ObjectProviderOpción por defecto: limpio, testeable, flexible
ObjectFactoryCuando solo necesitas getObject()
@LookupBeans escaneados por componentes donde quieres código mínimo
Scoped ProxyBeans Request/Session que necesitan resolución transparente

Para la mayoría de casos, mi recomendación es ObjectProvider. Mantiene tu código testeable, no te acopla a las interioridades de Spring, y proporciona métodos útiles para dependencias opcionales.

Para más información sobre patrones de inyección y testing de aplicaciones Spring, consulta mis otros tutoriales.

El código fuente completo con ejemplos ejecutables con JBang está disponible 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 - 2026 Marc Nuri