【问题标题】:Inject spring bean dynamically动态注入spring bean
【发布时间】:2017-02-27 12:55:32
【问题描述】:

在 java-spring web-app 中,我希望能够动态注入 bean。 例如,我有一个具有 2 个不同实现的接口:

在我的应用中,我使用一些属性文件来配置注入:

#Determines the interface type the app uses. Possible values: implA, implB
myinterface.type=implA

我的注入实际上是根据属性文件中的属性值有条件地加载的。例如,在这种情况下,myinterface.type=implA 无论我在哪里注入 MyInterface,将被注入的实现都是 ImplA(我通过扩展 Conditional annotation 实现了这一点)。

我希望在运行时 - 一旦更改属性,将发生以下情况(无需重新启动服务器):

  1. 将注入正确的实现。例如,当设置myinterface.type=implB时,ImplB 将被注入到任何使用 MyInterface 的地方
  2. Spring Environment 应该用新值刷新并重新注入到 bean 中。

我想刷新我的上下文,但这会产生问题。 我想也许可以使用设置器进行注入,并在重新配置属性后重新使用这些设置器。有这种要求的工作实践吗?

有什么想法吗?

更新

正如一些人建议的那样,我可以使用一个工厂/注册表,它包含两个实现(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 而不是构造函数,但这会降低性能,而且我想考虑一个用于重新注入环境的 ider(再次,也许使用 setter 注入?)。

【问题讨论】:

  • 如何将属性中的值更改为 myinterface.type=implB,以及系统如何知道您完成/完成更改了值
  • 此外,应用程序应该如何判断已经注入的依赖项在使用中并且您强制交换实现的情况?这种方法的主要目的是什么?
  • @kuhajeyan 假设我知道(例如使用 WatchService),这不是这里的挑战。
  • @forhas,请查看此链接:stackoverflow.com/questions/12800769/…。它可能对你有帮助。但它真正黑暗的方式是在运行时交换 bean 实现。您的问题应该通过正确的设计模式来解决。
  • @forhas,类似问题有很好的答案:stackoverflow.com/questions/21221125/…

标签: java spring polymorphism spring-environment


【解决方案1】:

我会让这个任务尽可能简单。我不是在启动时有条件地加载MyInterface 接口的一个实现,然后触发一个触发同一接口的另一个实现的动态加载的事件,而是以不同的方式解决这个问题,这更容易实现和维护。

首先,我将加载所有可能的实现:

@Component
public class MyInterfaceImplementationsHolder {

    @Autowired
    private Map<String, MyInterface> implementations;

    public MyInterface get(String impl) {
        return this.implementations.get(impl);
    }
}

这个 bean 只是 MyInterface 接口的所有实现的持有者。这里没什么神奇的,只是常见的 Spring 自动装配行为。

现在,无论您需要在哪里注入 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 来实际更改每个具有 MyInterface 作为属性的 bean 中的实现:

@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 接口的bean,并使用新实现更新它们中的每一个,新实现从持有者中检索并作为参数传递。同样,常见的 Spring 自动装配规则。

当你想改变实现时,你应该只调用updateImplementations方法和新实现的bean的名字,这是类的小写驼峰式简单名称,即myImplA或@ 987654334@ 用于课程 MyImplAMyImplB

您还应该在启动时调用此方法,以便在每个实现 MyInterfaceReloader 接口的 bean 上设置初始实现。

【讨论】:

  • 看来这是一个有趣的解决方案。它不使用任何"Spring magic",有些人可能会喜欢这个。至于我,我喜欢Springsolution with proxy 使用了"magic" 的所有力量。
  • 我不会使用这种方法,因为它使每个客户端都带有实现切换的概念。这正是动态代理的用例。那么为什么不使用它:-)
  • @JohannesLeimer 好吧,也许 clutter 说得太多了,不是吗?客户端只需要实现MyInterfaceReloader 接口,它只设置一个属性。动态代理是一个很好的方法,不过,我同意你的观点。
  • 我们需要有自己想法的人,而不是仅仅在如何使用现有库上胡思乱想?谢谢!费德里科。至少我可以在这里用更少的代码示例了解什么是动态代理实现。
【解决方案2】:

我通过使用 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();
    }
}

希望对你有帮助。

【讨论】:

    【解决方案3】:

    如果我理解正确,那么目标不是替换注入的对象实例,而是在接口方法调用期间使用不同的实现取决于运行时的某些条件。

    如果是这样,那么您可以尝试结合ProxyFactoryBean 来查看Sring TargetSource 机制。重点是代理对象会被注入到使用你接口的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 bean,并且 Spring 上下文不能同时包含两个实例,那么您可以尝试将 ServiceLocatorFactoryBeanprototype 目标 bean 范围和 @目标实现类上的 987654331@ 注释。可以使用这种方法代替MyRegistry

    附言 可能 Application Context 刷新操作也可以做你想做的,但它可能会导致其他问题,例如性能开销。

    【讨论】:

      【解决方案4】:

      这可能是一个重复的问题,或者至少非常相似,无论如何我在这里回答了这类问题:Spring bean partial autowire prototype constructor

      几乎当你想要在运行时为依赖项使用不同的 bean 时,你需要使用原型作用域。然后您可以使用配置返回原型 bean 的不同实现。您将需要处理返回自己的实现的逻辑,(他们甚至可以返回 2 个不同的单例 bean 没关系)但是假设您想要新的 bean,并且返回实现的逻辑在一个名为 @ 的 bean 中987654322@,那么就可以进行配置了:

      @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 真的是单例 bean。您不应该重新加载上下文来重新创建单例 bean 来实现不同的运行时行为,这不是必需的。

      编辑 这个答案的第一部分回答了关于动态注入 bean 的问题。如所问,但我认为问题更多的是:'如何在运行时更改单例 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();
              }
          }   
      }
      

      【讨论】:

      • 您对应用程序上下文重新加载带来的不必要开销是正确的,但注入 bean 的动态行为更改问题是绝对合法的,这种设计在 OOP 中称为 Polymorphism。关于您的答案,它与下面另一个答案中提出的@Conditional 注释非常相似。
      • @SergeyBespalov 不,多态性不是关于特定对象在运行时表现不同的问题。一个对象只会表现不同,因为它的状态已经改变。此外,我的答案与下面的不同,因为不需要重新加载上下文。某些引用是否已从一种实现更改为另一种实现,您可能会在那里谈论多态性,但我们不是。我们正在谈论 Spring 和依赖注入。
      • 我的意思是不应该重新创建单例 bean。如果出于某种疯狂的原因你想重新创建一个单例 bean,它应该只具有原型范围,并且你的配置应该处理给出的实现。如果您想在运行时更改单例 bean 的实现,请更改状态,不要重新创建它。
      • Polymorphism 正是关于这个,我在这里看不到任何State
      • 不,不是。它是关于满足接口的多个类。一个特定的对象(实例)只有在其状态发生变化时才会改变其行为。例如 Bean.doStuff(),只有在 todo 发生变化或者 implA 或 implB 中的数据发生变化时才会有所不同。 MyInterface 可以有不同的实现,这是因为多态性。但是 doStuff 发生了变化,因为状态发生了变化。你很困惑。
      【解决方案5】:

      你可以使用@Resource注解注入原来回答here

      例如

      @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;
      
      }
      

      然后将properties文件中的实现类型设置为-

      myinterface.type=implA
      

      【讨论】:

        【解决方案6】:

        请注意 - 如果有兴趣了解的话 - FileChangedReloadingStrategy 使您的项目高度依赖于部署条件:WAR/EAR 应该由容器展开,并且您应该可以直接访问文件系统,这些条件并不总是满足在所有情况和环境中。

        【讨论】:

          【解决方案7】:

          您可以在属性值上使用 Spring @Conditional。给两个 Bean 赋予相同的名称,它应该可以工作,因为只会创建一个实例。

          在这里查看如何在服务和组件上使用@Conditional: http://blog.codeleak.pl/2015/11/how-to-register-components-using.html

          【讨论】:

          • 好的,这是否解决了我在运行时更改注入元素的要求?
          • 嗯,我想你可能不得不用这个重新加载上下文。
          • 现在你在说话。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-07-05
          • 2011-01-05
          • 1970-01-01
          • 2019-08-11
          • 2012-02-19
          相关资源
          最近更新 更多