Dans une application web Java-spring, j'aimerais pouvoir injecter dynamiquement des beans. Par exemple, j'ai une interface avec 2 implémentations différentes:
Dans mon application, j'utilise un fichier de propriétés pour configurer les injections:
#Determines the interface type the app uses. Possible values: implA, implB
myinterface.type=implA
Mes injections se sont effectivement chargées conditionnellement en relayant les valeurs des propriétés dans le fichier de propriétés. Par exemple, dans ce cas, myinterface.type = implA partout où j'injecte MyInterface, l'implémentation qui sera injectée sera ImplA (j'ai accompli cela en étendant annotation conditionnelle ).
J'aimerais cela pendant l'exécution - une fois les propriétés modifiées, les événements suivants se produiront (sans redémarrage du serveur):
myinterface.type=implB
ImplB sera injecté partout où MyInterface est utiliséJ'ai pensé rafraîchir mon contexte mais cela crée des problèmes. J'ai pensé peut-être utiliser des setters pour l'injection et réutiliser ces setters une fois les propriétés reconfigurées. Existe-t-il une pratique de travail pour une telle exigence?
Des idées?
[~ # ~] mise à jour [~ # ~]
Comme certains l'ont suggéré, je peux utiliser une fabrique/registre qui contient les deux implémentations (ImplA et ImplB) et renvoie la bonne en interrogeant la propriété appropriée. Si je fais cela, j'ai toujours le deuxième défi - l'environnement. par exemple si mon registre ressemble à ceci:
@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;
}
}
}
Une fois que la propriété a changé, je dois réinjecter mon environnement. des suggestions pour ça?
Je sais que je peux interroger cet env à l'intérieur de la méthode au lieu du constructeur, mais c'est une réduction des performances et je voudrais également penser à un ider pour la réinjection de l'environnement (encore une fois, peut-être en utilisant une injection de setter?).
Je garderais cette tâche aussi simple que possible. Au lieu de charger conditionnellement une implémentation de l'interface MyInterface
au démarrage puis de déclencher un événement qui déclenche le chargement dynamique d'une autre implémentation de la même interface, j'attaquerais ce problème d'une manière différente, qui est beaucoup plus simple à implémenter et maintenir.
Tout d'abord, je chargerais simplement toutes les implémentations possibles:
@Component
public class MyInterfaceImplementationsHolder {
@Autowired
private Map<String, MyInterface> implementations;
public MyInterface get(String impl) {
return this.implementations.get(impl);
}
}
Ce bean n'est qu'un support pour toutes les implémentations de l'interface MyInterface
. Rien de magique ici, juste un comportement de câblage automatique Spring commun.
Maintenant, partout où vous devez injecter une implémentation spécifique de MyInterface
, vous pouvez le faire à l'aide d'une interface:
public interface MyInterfaceReloader {
void changeImplementation(MyInterface impl);
}
Ensuite, pour chaque classe qui doit être notifiée d'un changement de l'implémentation, faites-lui simplement implémenter l'interface MyInterfaceReloader
. Par exemple:
@Component
public class SomeBean implements MyInterfaceReloader {
// Do not autowire
private MyInterface myInterface;
@Override
public void changeImplementation(MyInterface impl) {
this.myInterface = impl;
}
}
Enfin, vous avez besoin d'un bean qui modifie réellement l'implémentation dans chaque bean ayant MyInterface
comme attribut:
@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)));
}
}
Cela transfère simplement automatiquement tous les beans qui implémentent l'interface MyInterfaceReloader
et met à jour chacun d'eux avec la nouvelle implémentation, qui est récupérée du titulaire et passée en argument. Encore une fois, les règles de câblage automatique Spring courantes.
Chaque fois que vous souhaitez que l'implémentation soit modifiée, vous devez simplement invoquer la méthode updateImplementations
avec le nom du bean de la nouvelle implémentation, qui est le nom simple inférieur de la classe des chameaux de la classe, c'est-à-dire myImplA
ou myImplB
pour les classes MyImplA
et MyImplB
.
Vous devez également appeler cette méthode au démarrage, afin qu'une implémentation initiale soit définie sur chaque bean qui implémente l'interface MyInterfaceReloader
.
J'ai résolu un problème similaire en utilisant org.Apache.commons.configuration.PropertiesConfiguration et org.springframework.beans.factory.config.ServiceLocatorFactoryBean:
Soit VehicleRepairService une interface:
public interface VehicleRepairService {
void repair();
}
et CarRepairService et TruckRepairService deux classes qui l'implémentent:
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");
}
}
Je crée une interface pour une fabrique de services:
public interface VehicleRepairServiceFactory {
VehicleRepairService getRepairService(String serviceType);
}
Soit Config comme classe de configuration:
@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();
}
}
En utilisant FileChangedReloadingStrategy votre configuration sera rechargée lorsque vous modifiez le fichier de propriétés.
service=truckRepairService
#service=carRepairService
Ayant la configuration et l'usine à votre service, vous pouvez obtenir le service approprié de l'usine en utilisant la valeur actuelle de la propriété.
@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();
}
}
J'espère que ça aide.
Si je vous comprends bien, le but n'est pas de remplacer les instances d'objets injectés mais d'utiliser différentes implémentations lors de l'appel de méthode d'interface dépend d'une condition au moment de l'exécution.
Si c'est le cas, vous pouvez essayer de regarder le mécanisme Sring
TargetSource en combinaison avec ProxyFactoryBean . Le fait est que les objets proxy seront injectés dans des beans qui utilisent votre interface, et tous les appels de méthode d'interface seront envoyés à la cible TargetSource
.
Appelons cela "proxy polymorphe".
Jetez un œil à l'exemple ci-dessous:
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();
}
}
De plus, si vous souhaitez que les deux implémentations MyInterface
soient des beans Spring et que le contexte Spring ne puisse pas contenir les deux instances en même temps, vous pouvez essayer d'utiliser ServiceLocatorFactoryBean avec prototype
portée des beans cibles et annotation Conditional
sur les classes d'implémentation cibles. Cette approche peut être utilisée à la place de MyRegistry
.
P.S. L'opération d'actualisation du contexte d'application peut également faire ce que vous voulez, mais elle peut entraîner d'autres problèmes tels que des frais généraux de performances.
Cela peut être une question en double ou au moins très similaire, de toute façon j'ai répondu à ce genre de question ici: constructeur de prototype partiel de câblage automatique Spring Bean
À peu près lorsque vous voulez un autre bean pour une dépendance au moment de l'exécution, vous devez utiliser une étendue de prototype. Vous pouvez ensuite utiliser une configuration pour renvoyer différentes implémentations du bean prototype. Vous devrez gérer vous-même la logique sur laquelle l'implémentation doit être renvoyée (ils pourraient même renvoyer 2 beans singleton différents, cela n'a pas d'importance). Mais dites que vous voulez de nouveaux beans, et la logique de retour de l'implémentation se trouve dans un bean appelé SomeBeanWithLogic.isSomeBooleanExpression()
, vous pouvez alors faire une configuration:
@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
}
}
}
Il ne devrait jamais être nécessaire de recharger le contexte. Si, par exemple, vous souhaitez que l'implémentation d'un bean change au moment de l'exécution, utilisez ce qui précède. Si vous avez vraiment besoin de recharger votre application, car ce bean a été utilisé dans les constructeurs d'un bean singleton ou quelque chose de bizarre, alors vous devez repenser votre conception, et si ces beans sont vraiment des beans singleton. Vous ne devez pas recharger le contexte pour recréer des beans singleton afin d'obtenir un comportement d'exécution différent, ce qui n'est pas nécessaire.
Modifier La première partie de cette réponse a répondu à la question sur l'injection dynamique de beans. Comme demandé, mais je pense que la question est plus d'une: "comment puis-je changer l'implémentation d'un bean singleton au moment de l'exécution". Cela pourrait être fait avec un modèle de conception 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();
}
}
}
Sachez que - si intéressant à savoir - FileChangedReloadingStrategy rend votre projet très dépendant des conditions de déploiement: le WAR/EAR doit être explosé par conteneur et vous devez avoir un accès direct au système de fichiers, conditions qui ne sont pas toujours remplies dans toutes les situations et les environnements.
Vous pouvez utiliser Spring @Conditional sur une valeur de propriété. Donnez aux deux Beans le même nom et cela devrait fonctionner car une seule instance sera créée.
Jetez un œil ici sur la façon d'utiliser @Conditional on Services and Components: http://blog.codeleak.pl/2015/11/how-to-register-components-using.html