java - resource - spring injection
Inyectar el frijol de primavera dinĂ¡micamente. (6)
En una aplicación web de Java-Spring, me gustaría poder inyectar beans de forma dinámica. Por ejemplo tengo una interfaz con 2 implementaciones diferentes:
En mi aplicación estoy usando algún archivo de propiedades para configurar inyecciones:
#Determines the interface type the app uses. Possible values: implA, implB
myinterface.type=implA
Mis inyecciones realmente se cargaron de forma condicional en los valores de propiedades en el archivo de propiedades. Por ejemplo, en este caso myinterface.type = implA donde inyecto MyInterface, la implementación que se inyectará será ImplA (lo logré extendiendo la anotación Condicional ).
Me gustaría que durante el tiempo de ejecución: una vez que se cambien las propiedades, sucederá lo siguiente (sin reinicio del servidor):
- Se inyectará la implementación correcta. Por ejemplo, cuando se configura
myinterface.type=implB
ImplB se inyectará donde quiera que se use MyInterface - Spring Environment debe actualizarse con los nuevos valores y reinyectarse también en frijoles.
Pensé en refrescar mi contexto pero eso crea problemas. Pensé que tal vez usar los setters para inyección y reutilizar esos setters una vez que las propiedades se reconfiguran. ¿Existe una práctica de trabajo para tal requisito?
¿Algunas ideas?
ACTUALIZAR
Como algunos sugirieron, puedo usar una fábrica / registro que contenga ambas implementaciones (ImplA e ImplB) y devuelva la correcta consultando la propiedad correspondiente. Si hago eso, todavía tengo el segundo desafío: el medio ambiente. por ejemplo, si mi registro se ve así:
@Service
public class MyRegistry {
private String configurationValue;
private final MyInterface implA;
private final MyInterface implB;
@Inject
public MyRegistry(Environmant env, MyInterface implA, MyInterface ImplB) {
this.implA = implA;
this.implB = implB;
this.configurationValue = env.getProperty("myinterface.type");
}
public MyInterface getMyInterface() {
switch(configurationValue) {
case "implA":
return implA;
case "implB":
return implB;
}
}
}
Una vez que la propiedad ha cambiado debo reinyectar mi entorno. ¿Alguna sugerencia para eso?
Sé que puedo consultar esa env dentro del método en lugar del constructor, pero esto es una reducción del rendimiento y también me gustaría pensar en un ider para reinyectar el entorno (de nuevo, ¿tal vez usar una inyección de ajuste?).
Esto puede ser una pregunta duplicada o al menos muy similar, de todos modos respondí a este tipo de pregunta aquí: constructor de prototipo de autowire parcial de Spring Bean
Más o menos, cuando desea un bean diferente para una dependencia en tiempo de ejecución, necesita utilizar un ámbito de prototipo. Luego puede usar una configuración para devolver diferentes implementaciones del bean prototipo. Necesitará manejar la lógica sobre la cual la implementación se devolverá, (incluso podrían estar devolviendo 2 beans singleton diferentes, no importa). Pero digamos que quiere nuevos beans, y la lógica para devolver la implementación está en un bean llamado SomeBeanWithLogic.isSomeBooleanExpression()
, entonces puede hacer una configuración:
@Configuration
public class SpringConfiguration
{
@Bean
@Autowired
@Scope("prototype")
public MyInterface createBean(SomeBeanWithLogic someBeanWithLogic )
{
if (someBeanWithLogic .isSomeBooleanExpression())
{
return new ImplA(); // I could be a singleton bean
}
else
{
return new ImplB(); // I could also be a singleton bean
}
}
}
Nunca debería haber una necesidad de volver a cargar el contexto. Si, por ejemplo, desea que la implementación de un bean cambie en tiempo de ejecución, use lo anterior. Si realmente necesita volver a cargar su aplicación, porque este bean se usó en constructores de frijoles singleton o algo raro, entonces necesita repensar su diseño, y si estos frijoles son realmente frijoles singleton. No debe volver a cargar el contexto para volver a crear beans de singleton para lograr un comportamiento de tiempo de ejecución diferente, que no es necesario.
Editar La primera parte de esta respuesta respondió a la pregunta sobre la inyección dinámica de frijoles. Según lo preguntado, pero creo que la pregunta es más de una: "¿Cómo puedo cambiar la implementación de un bean singleton en tiempo de ejecución?". Esto podría hacerse con un patrón de diseño proxy.
interface MyInterface
{
public String doStuff();
}
@Component
public class Bean implements MyInterface
{
boolean todo = false; // change me as needed
// autowire implementations or create instances within this class as needed
@Qualifier("implA")
@Autowired
MyInterface implA;
@Qualifier("implB")
@Autowired
MyInterface implB;
public String doStuff()
{
if (todo)
{
return implA.doStuff();
}
else
{
return implB.doStuff();
}
}
}
Mantendría esta tarea lo más simple posible. En lugar de cargar condicionalmente una implementación de la interfaz MyInterface
al inicio y luego MyInterface
un evento que active la carga dinámica de otra implementación de la misma interfaz, abordaría este problema de una manera diferente, que es mucho más simple de implementar y mantener.
En primer lugar, acabo de cargar todas las implementaciones posibles:
@Component
public class MyInterfaceImplementationsHolder {
@Autowired
private Map<String, MyInterface> implementations;
public MyInterface get(String impl) {
return this.implementations.get(impl);
}
}
Este bean es solo un soporte para todas las implementaciones de la interfaz MyInterface
. Aquí no hay nada mágico, solo el comportamiento común de la alimentación automática de Spring
Ahora, donde sea que necesite inyectar una implementación específica de MyInterface
, puede hacerlo con la ayuda de una interfaz:
public interface MyInterfaceReloader {
void changeImplementation(MyInterface impl);
}
Luego, para cada clase que deba ser notificada de un cambio en la implementación, simplemente haga que implemente la interfaz MyInterfaceReloader
. Por ejemplo:
@Component
public class SomeBean implements MyInterfaceReloader {
// Do not autowire
private MyInterface myInterface;
@Override
public void changeImplementation(MyInterface impl) {
this.myInterface = impl;
}
}
Finalmente, necesita un bean que realmente cambie la implementación en cada bean que tenga MyInterface
como un atributo:
@Component
public class MyInterfaceImplementationUpdater {
@Autowired
private Map<String, MyInterfaceReloader> reloaders;
@Autowired
private MyInterfaceImplementationsHolder holder;
public void updateImplementations(String implBeanName) {
this.reloaders.forEach((k, v) ->
v.changeImplementation(this.holder.get(implBeanName)));
}
}
Esto simplemente crea automáticamente todos los beans que implementan la interfaz MyInterfaceReloader
y actualiza cada uno de ellos con la nueva implementación, que se recupera del titular y se pasa como un argumento. Una vez más, las normas comunes de primavera de autowiring.
Cuando quiera que se cambie la implementación, simplemente debe invocar el método updateImplementations
con el nombre del bean de la nueva implementación, que es el nombre simple de la clase del camello inferior, es decir, myImplA
o myImplB
para las clases MyImplA
y MyImplB
.
También debe invocar este método al inicio, para que se establezca una implementación inicial en cada bean que implementa la interfaz MyInterfaceReloader
.
Puede utilizar Spring @Conditional en un valor de propiedad. Asigne a ambos Beans el mismo nombre y debería funcionar ya que solo se creará una Instancia.
Eche un vistazo aquí sobre cómo usar @Conditional en Servicios y Componentes: http://blog.codeleak.pl/2015/11/how-to-register-components-using.html
Resolví un problema similar utilizando org.apache.commons.configuration.PropertiesConfiguration y org.springframework.beans.factory.config.ServiceLocatorFactoryBean:
Deje que VehicleRepairService sea una interfaz:
public interface VehicleRepairService {
void repair();
}
y CarRepairService y TruckRepairService dos clases que lo implementan:
public class CarRepairService implements VehicleRepairService {
@Override
public void repair() {
System.out.println("repair a car");
}
}
public class TruckRepairService implements VehicleRepairService {
@Override
public void repair() {
System.out.println("repair a truck");
}
}
Creo una interfaz para una fábrica de servicios:
public interface VehicleRepairServiceFactory {
VehicleRepairService getRepairService(String serviceType);
}
Vamos a usar Config como clase de configuración:
@Configuration()
@ComponentScan(basePackages = "config.test")
public class Config {
@Bean
public PropertiesConfiguration configuration(){
try {
PropertiesConfiguration configuration = new PropertiesConfiguration("example.properties");
configuration
.setReloadingStrategy(new FileChangedReloadingStrategy());
return configuration;
} catch (ConfigurationException e) {
throw new IllegalStateException(e);
}
}
@Bean
public ServiceLocatorFactoryBean serviceLocatorFactoryBean() {
ServiceLocatorFactoryBean serviceLocatorFactoryBean = new ServiceLocatorFactoryBean();
serviceLocatorFactoryBean
.setServiceLocatorInterface(VehicleRepairServiceFactory.class);
return serviceLocatorFactoryBean;
}
@Bean
public CarRepairService carRepairService() {
return new CarRepairService();
}
@Bean
public TruckRepairService truckRepairService() {
return new TruckRepairService();
}
@Bean
public SomeService someService(){
return new SomeService();
}
}
Al utilizar FileChangedReloadingStrategy, su configuración se volverá a cargar cuando cambie el archivo de propiedades.
service=truckRepairService
#service=carRepairService
Con la configuración y la fábrica a su servicio, le permite obtener el servicio apropiado de la fábrica utilizando el valor actual de la propiedad.
@Service
public class SomeService {
@Autowired
private VehicleRepairServiceFactory factory;
@Autowired
private PropertiesConfiguration configuration;
public void doSomething() {
String service = configuration.getString("service");
VehicleRepairService vehicleRepairService = factory.getRepairService(service);
vehicleRepairService.repair();
}
}
Espero eso ayude.
Si lo entiendo correctamente, el objetivo no es reemplazar las instancias de objetos inyectados, sino usar diferentes implementaciones durante la llamada del método de interfaz, depende de alguna condición en el tiempo de ejecución.
Si es así, puede intentar ver el mecanismo Sring
TargetSource en combinación con ProxyFactoryBean . El punto es que los objetos proxy se inyectarán a los beans que usan su interfaz, y todas las llamadas de método de interfaz se enviarán al destino TargetSource
.
Llamemos a esto "Proxy Polimórfico".
Echa un vistazo al siguiente ejemplo:
ConditionalTargetSource.java
@Component
public class ConditionalTargetSource implements TargetSource {
@Autowired
private MyRegistry registry;
@Override
public Class<?> getTargetClass() {
return MyInterface.class;
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() throws Exception {
return registry.getMyInterface();
}
@Override
public void releaseTarget(Object target) throws Exception {
//Do some staff here if you want to release something related to interface instances that was created with MyRegistry.
}
}
applicationContext.xml
<bean id="myInterfaceFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="MyInterface"/>
<property name="targetSource" ref="conditionalTargetSource"/>
</bean>
<bean name="conditionalTargetSource" class="ConditionalTargetSource"/>
SomeService.java
@Service
public class SomeService {
@Autowired
private MyInterface myInterfaceBean;
public void foo(){
//Here we have `myInterfaceBean` proxy that will do `conditionalTargetSource.getTarget().bar()`
myInterfaceBean.bar();
}
}
Además, si desea que las implementaciones de MyInterface
sean Spring beans y el contexto Spring no pueda contener ambas instancias al mismo tiempo, puede intentar usar ServiceLocatorFactoryBean con alcance de beans de prototype
objetivo y anotaciones Conditional
en las clases de implementación de destino. Este enfoque se puede utilizar en lugar de MyRegistry
.
PS Probablemente, la operación de actualización del Contexto de la aplicación también puede hacer lo que quiera, pero puede causar otros problemas, como los gastos generales de rendimiento.
Tenga en cuenta que, si es interesante saberlo, FileChangedReloadingStrategy hace que su proyecto sea altamente dependiente de las condiciones de implementación: el WAR / EAR debe explotarse por contenedor y debe tener acceso directo al sistema de archivos, condiciones que no siempre se cumplen en todas las situaciones y ambientes.