Инъекция spring bean динамически
В веб-приложении java- spring я хотел бы динамически вводить beans.
Например, у меня есть интерфейс с двумя различными реализациями:
![введите описание изображения здесь]()
В моем приложении я использую файл свойств для настройки инъекций:
#Determines the interface type the app uses. Possible values: implA, implB
myinterface.type=implA
Мои инъекции, фактически загруженные, условно передают значения свойств в файле свойств. Например, в этом случае myinterface.type = implA, где бы я ни вводил MyInterface, реализация, которая будет введена, будет ImplA (я достиг этого, расширив Условную аннотацию).
Мне бы хотелось, чтобы во время выполнения - после изменения свойств произойдет следующее (без перезагрузки сервера):
- Будет введена правильная реализация. Например, при установке
myinterface.type=implB
ImplB будет вводиться, когда используется MyInterface
- Spring Окружающая среда должна обновляться с новыми значениями и повторно вводиться также в beans.
Я думал об обновлении моего контекста, но это создает проблемы.
Я подумал, может быть, использовать сеттеры для инъекций и повторно использовать эти сеттеры после повторной настройки свойств. Существует ли рабочая практика для такого требования?
Любые идеи?
UPDATE
Как я уже сказал, я могу использовать реестр factory/, который содержит обе реализации (ImplA и ImplB) и возвращает правильный, запрашивая соответствующее свойство.
Если я это сделаю, у меня все еще есть вторая проблема - окружающая среда. например, если мой реестр выглядит следующим образом:
@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;
}
}
}
Как только свойство изменилось, я должен повторно ввести свою среду. любые предложения для этого?
Я знаю, что могу запросить это env внутри метода вместо конструктора, но это снижение производительности, и я также хотел бы подумать об идре для повторной инъекции среды (опять же, возможно, используя инъекцию setter?).
Ответы
Ответ 1
Я решил аналогичную проблему, используя org.apache.commons.configuration.PropertiesConfiguration и org.springframework.beans.factory.config.ServiceLocatorFactoryBean:
Пусть VehicleRepairService будет интерфейсом:
public interface VehicleRepairService {
void repair();
}
и CarRepairService и TruckRepairService два класса, которые его реализуют:
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");
}
}
Я создаю интерфейс для службы factory:
public interface VehicleRepairServiceFactory {
VehicleRepairService getRepairService(String serviceType);
}
Используйте Config в качестве класса конфигурации:
@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();
}
}
С помощью FileChangedReloadingStrategy ваша конфигурация перезагружается при изменении файла свойств.
service=truckRepairService
#service=carRepairService
Имея конфигурацию и factory в вашей службе, позвольте вам получить соответствующую услугу из factory, используя текущее значение свойства.
@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();
}
}
Надеюсь, что это поможет.
Ответ 2
Я бы поставил задачу как можно проще. Вместо условной загрузки одной реализации интерфейса MyInterface
при запуске, а затем запускайте событие, которое запускает динамическую загрузку другой реализации того же интерфейса, я бы решил эту проблему по-другому, что намного проще реализовать и поддерживать.
Прежде всего, я бы просто загрузил все возможные реализации:
@Component
public class MyInterfaceImplementationsHolder {
@Autowired
private Map<String, MyInterface> implementations;
public MyInterface get(String impl) {
return this.implementations.get(impl);
}
}
Этот bean является просто держателем для всех реализаций интерфейса MyInterface
. Ничего волшебного здесь, просто общее поведение Spring autowiring.
Теперь, когда вам нужно ввести конкретную реализацию MyInterface
, вы можете сделать это с помощью интерфейса:
public interface MyInterfaceReloader {
void changeImplementation(MyInterface impl);
}
Затем для каждого класса, который должен быть уведомлен об изменении реализации, просто заставьте его реализовать интерфейс MyInterfaceReloader
. Например:
@Component
public class SomeBean implements MyInterfaceReloader {
// Do not autowire
private MyInterface myInterface;
@Override
public void changeImplementation(MyInterface impl) {
this.myInterface = impl;
}
}
Наконец, вам понадобится bean, который фактически изменяет реализацию в каждом bean, который имеет MyInterface
в качестве атрибута:
@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)));
}
}
Это просто autwires все beans, которые реализуют интерфейс MyInterfaceReloader
и обновляют каждую из них с помощью новой реализации, которая извлекается из держателя и передается в качестве аргумента. Опять же, общие правила Spring autowiring.
Всякий раз, когда вы хотите, чтобы реализация была изменена, вы должны просто вызвать метод updateImplementations
с именем bean новой реализации, являющейся простым именем класса верблюда, т.е. myImplA
или myImplB
для классов myImplA
и myImplB
.
Вы также должны вызывать этот метод при запуске, чтобы на каждом bean, который реализует интерфейс MyInterfaceReloader
, устанавливается начальная реализация.
Ответ 3
Если вы правильно понимаете, то цель заключается не в замене инъецируемых экземпляров объектов, а в использовании разных реализаций во время вызова метода интерфейса, зависит от некоторого состояния во время выполнения.
Если это так, вы можете попытаться взглянуть на механизм Sring
TargetSource в сочетании с ProxyFactoryBean. Дело в том, что объекты-прокси будут добавлены в beans, который использует ваш интерфейс, и все вызовы метода интерфейса будут отправлены в TargetSource
target.
Позвольте называть этот "Полиморфный прокси".
Посмотрите пример ниже:
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();
}
}
Также, если вы хотите, чтобы обе реализации MyInterface
были Spring beans, а контекст Spring не мог содержать оба экземпляра одновременно, вы можете попытаться использовать ServiceLocatorFactoryBean с prototype
целевой областью beans и Conditional
аннотацией к целевым классам реализации. Этот подход можно использовать вместо MyRegistry
.
P.S.
Вероятно, операция обновления контекста приложения также может делать то, что вы хотите, но может вызвать другие проблемы, такие как накладные расходы на производительность.
Ответ 4
Это может быть дублированный вопрос или, по крайней мере, очень похожий, в любом случае я ответил на этот вопрос здесь: Spring bean конструктор прототипа частичного autowire
В значительной степени, когда вы хотите использовать другой beans для зависимости во время выполнения, вам нужно использовать область прототипа. Затем вы можете использовать конфигурацию для возврата различных реализаций прототипа bean. Вам нужно будет обработать логику, на которой реализация вернется сама собой (они могут даже возвращать 2 разных singleton beans, это не имеет значения). Но скажите, что вы хотите новый beans, а логика возврата реализации в bean называется SomeBeanWithLogic.isSomeBooleanExpression()
, тогда вы можете сделать конфигурацию:
@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
}
}
}
Никогда не нужно перезагружать контекст. Если, например, вы хотите, чтобы реализация bean менялась во время выполнения, используйте приведенное выше. Если вам действительно нужно перезагрузить приложение, потому что этот bean использовался в конструкторах singleton bean или что-то странное, тогда вам нужно переосмыслить свой дизайн, и если эти beans действительно одиночные beans, Вы не должны перезагружать контекст для воссоздания singleton beans для достижения разного поведения во время выполнения, что не требуется.
Изменить. Первая часть этого ответа ответила на вопрос о динамическом введении beans. Как и было задано, но я думаю, что вопрос более один: "Как я могу изменить реализацию singleton bean во время выполнения". Это можно сделать с помощью шаблона прокси-сервера.
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();
}
}
}
Ответ 5
Вы можете использовать Spring @Conditional для значения свойства. Дайте оба Beans одно и то же имя, и оно должно работать только при создании только одного экземпляра.
Посмотрите здесь, как использовать @Conditional для служб и компонентов:
http://blog.codeleak.pl/2015/11/how-to-register-components-using.html
Ответ 6
Имейте в виду, что - если интересно узнать - FileChangedReloadingStrategy делает ваш проект сильно зависимым от условий развертывания: WAR/EAR должен быть взорван контейнером, и ваш должен иметь прямой доступ к файловой системе, условия, которые не всегда выполняются во всех ситуациях и средах.