Внедрить бобов весной динамически
В веб-приложении java-spring я хотел бы иметь возможность динамически внедрять бины. Например, у меня есть интерфейс с 2 различными реализациями:
В моем приложении я использую файл свойств для настройки инъекций:
#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 Environment должен быть обновлен с новыми значениями и повторно введен в бобы.
Я думал об обновлении своего контекста, но это создает проблемы. Я подумал, может быть, использовать сеттеры для инъекций и повторно использовать эти сеттеры, как только свойства будут переконфигурированы. Есть ли рабочая практика для такого требования?
Есть идеи?
ОБНОВИТЬ
Как некоторые предположили, я могу использовать фабрику / реестр, который содержит обе реализации (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 внутри метода вместо конструктора, но это снижение производительности, а также я хотел бы подумать об идентификаторе для повторного внедрения среды (опять же, может быть, с использованием инъекции сеттера?).
8 ответов
Я бы сделал эту задачу максимально простой. Вместо условной загрузки одной реализации MyInterface
Интерфейс при запуске, а затем запускает событие, которое запускает динамическую загрузку другой реализации того же интерфейса, я бы решил эту проблему по-другому, гораздо проще реализовать и поддерживать.
Прежде всего, я бы просто загрузил все возможные реализации:
@Component
public class MyInterfaceImplementationsHolder {
@Autowired
private Map<String, MyInterface> implementations;
public MyInterface get(String impl) {
return this.implementations.get(impl);
}
}
Этот бин является просто держателем для всех реализаций MyInterface
интерфейс. Здесь нет ничего волшебного, только обычное поведение Spring Autopiring.
Теперь, где вам нужно внедрить конкретную реализацию 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;
}
}
Наконец, вам нужен компонент, который фактически меняет реализацию в каждом компоненте, который имеет 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)));
}
}
Это просто автоматически связывает все бины, которые реализуют MyInterfaceReloader
интерфейс и обновляет каждый из них с новой реализацией, которая извлекается из держателя и передается в качестве аргумента. Опять же, общие правила электромонтажа Spring.
Всякий раз, когда вы хотите, чтобы реализация была изменена, вы должны просто вызвать updateImplementations
метод с именем bean-компонента новой реализации, который является простым именем класса в нижнем регистре верблюдов, т.е. myImplA
или же myImplB
для занятий MyImplA
а также MyImplB
,
Вы также должны вызывать этот метод при запуске, чтобы начальная реализация была установлена для каждого компонента, который реализует MyInterfaceReloader
интерфейс.
Я решил похожую проблему, используя 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");
}
}
Я создаю интерфейс для сервисной фабрики:
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
Имея конфигурацию и завод в вашем сервисе, вы можете получить соответствующий сервис от завода, используя текущее значение свойства.
@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();
}
}
Надеюсь, поможет.
Если я вас правильно понимаю, то цель не в том, чтобы заменить внедренные экземпляры объекта, а в том, чтобы использовать различные реализации во время вызова метода интерфейса, зависит от некоторых условий во время выполнения.
Если это так, то вы можете попытаться взглянуть на Sring
Механизм TargetSource в сочетании с ProxyFactoryBean. Дело в том, что прокси-объекты будут внедрены в bean-компоненты, использующие ваш интерфейс, и все вызовы метода интерфейса будут отправлены TargetSource
цель.
Давайте назовем это "Полиморфный прокси".
Посмотрите на пример ниже:
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, и контекст Spring не может содержать оба экземпляра одновременно, тогда вы можете попробовать использовать ServiceLocatorFactoryBean с prototype
целевой объем бобов и Conditional
аннотация о целевых классах реализации. Этот подход можно использовать вместо MyRegistry
,
PS Вероятно, операция обновления контекста приложения также может делать то, что вы хотите, но это может вызвать другие проблемы, такие как снижение производительности.
Это может быть повторяющийся вопрос или, по крайней мере, очень похожий, в любом случае, я ответил на этот вопрос здесь: Конструктор прототипа частичного автопровода bean-компонента Spring
В значительной степени, когда вам нужны разные bean-компоненты для зависимости во время выполнения, вам нужно использовать область действия прототипа. Затем вы можете использовать конфигурацию для возврата различных реализаций компонента-прототипа. Вам нужно будет обработать логику, по которой реализация должна возвращать себя (они могут даже возвращать 2 разных одноэлементных компонента, это не имеет значения) Но, скажем, вам нужны новые компоненты, и логика для возврата реализации находится в компоненте, называемом 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-компонента или чего-то странного, тогда вам нужно переосмыслить свой дизайн, и если эти bean-компоненты действительно являются bean-компонентами singleton. Вы не должны перезагружать контекст для повторного создания одноэлементных компонентов для достижения другого поведения во время выполнения, в этом нет необходимости.
Править Первая часть этого ответа ответила на вопрос о динамическом введении бобов. Как и спрашивали, но я думаю, что вопрос скорее в одном: "как я могу изменить реализацию одиночного компонента во время выполнения". Это можно сделать с помощью шаблона прокси-дизайна.
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();
}
}
}
Вы можете использовать аннотацию @Resource для инъекции, как первоначально ответили здесь
например
@Component("implA")
public class ImplA implements MyInterface {
...
}
@Component("implB")
public class ImplB implements MyInterface {
...
}
@Component
public class DependentClass {
@Resource(name = "\${myinterface.type}")
private MyInterface impl;
}
а затем установите тип реализации в файле свойств как -
myinterface.type=implA
Имейте в виду, что - если интересно узнать - FileChangedReloadingStrategy делает ваш проект в значительной степени зависимым от условий развертывания: WAR/EAR должен быть взорван контейнером, и вы должны иметь прямой доступ к файловой системе, условия, которые не всегда выполняются во всех ситуациях и среды.
Вы можете использовать Spring @Conditional для значения свойства. Дайте обоим Бинам одинаковое имя, и оно должно работать, так как будет создан только один Экземпляр.
Посмотрите здесь, как использовать @Conditional для служб и компонентов: http://blog.codeleak.pl/2015/11/how-to-register-components-using.html
public abstract class SystemService {
}
public class FooSystemService extends FileSystemService {
}
public class GoSystemService extends FileSystemService {
}
@Configuration
public class SystemServiceConf {
@Bean
@Conditional(SystemServiceCondition.class)
public SystemService systemService(@Value("${value.key}") value) {
switch (value) {
case A:
return new FooSystemService();
case B:
return new GoSystemService();
default:
throw new RuntimeException("unknown value ");
}
}
}
public class SystemServiceCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return true;
}
}