Beans prototype en singletons
- Alcance de bean de Spring singleton vs prototipo
- Inyectar bean prototipo en singleton en Spring
- Ejemplo de anotación @Lookup de Spring
- ObjectFactory y Provider en Spring
- Cómo obtener nueva instancia de bean prototipo en Spring
- ApplicationContext de Spring getBean para alcance prototipo
Alcances de bean de Spring
Primero lo primero – recordemos los tipos de alcance cruciales para los beans de Spring: singleton, prototype, request, session.
Beans singleton (alcance por defecto) tienen solo una instancia compartida del bean por contenedor IoC de Spring.
Beans prototipo son creados cada vez que son solicitados desde el contenedor.
Problema de inyección
Supongamos que tenemos este bean prototipo:
@Service
@Scope(scopeName = "prototype")
public class MyPrototypeBean {
private final UUID uuid = UUID.randomUUID();
public String getId() {
return uuid.toString();
}
}
Y este controlador REST, que inyecta y usa MyPrototypeBean:
@RestController
public class MySingletonBean {
@Autowired
private MyPrototypeBean prototypeBean;
@GetMapping("/")
public String id() {
return prototypeBean.getId();
}
}
Cuando ejecutamos este código, encontraremos que el id siempre será el mismo. ¿Por qué?
Porque las dependencias de un bean singleton se resuelven una vez, incluso si son prototipos.
Primera solución: uso manual de ApplicationContext
Podemos inyectar ApplicationContext (o BeanFactory) y obtener el bean prototipo desde el contenedor:
@RestController
public class MySingletonBean {
@Autowired
private ApplicationContext appContext;
@GetMapping("/")
public String id() {
return appContext.getBean(MyPrototypeBean.class).getId();
}
}
Segunda solución: @Lookup
En esencia, esta solución es una mejora para la primera.
La anotación @Lookup en Spring le dice al framework que reemplace dinámicamente una llamada a método con lógica que devuelve una nueva instancia de bean desde el contexto de Spring – incluso si el bean tiene alcance prototipo, y lo estamos llamando desde una clase con alcance singleton.
Si definimos un método abstracto usando @Lookup, Spring generará la implementación en tiempo de ejecución:
@Lookup
abstract MyPrototypeBean getMyPrototypeBean();
Deberíamos usarlo de la siguiente manera:
MyPrototypeBean myPrototypeBean = getMyPrototypeBean();
String id = myPrototypeBean.getId();
Notemos que getMyPrototypeBean() es abstract y no puede ser private. También, tengamos en cuenta que la clase de servicio también debería ser abstract.
Tercera solución: ObjectFactory<T>
¿Cuándo podríamos querer usar ObjectFactory? Bueno, es especialmente conveniente para las pruebas: ObjectFactory tiene un método getObject() que puede ser fácilmente simulado.
Pero ¿por qué no usar inyección por setter entonces? La respuesta es que si usamos inyección por setter – entonces las características de prototipo de MyPrototypeBean desaparecen.
@RestController
public class MySingletonBean {
@Autowired
private ObjectFactory<MyPrototypeBean> objectFactory;
@GetMapping("/")
public String id() {
return objectFactory.getObject().getId();
}
}
Cuarta opción: Provider<T>
Las razones para usar Provider<T> son las mismas que para ObjectFactory<T>.
Requiere la dependencia jakarta.inject:
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
Y se ve así:
@RestController
public class MySingletonBean {
@Autowired
private Provider<MyPrototypeBean> provider;
@GetMapping("/")
public String id() {
return provider.get().getId();
}
}