动态注入Spring Bean

36
在一个Java Spring Web应用程序中,我希望能够动态注入Bean。例如,我有一个接口,它有两个不同的实现:

enter image description here

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

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

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

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

  1. 正确的实现将被注入。例如,当设置myinterface.type=implB时,无论在哪里使用MyInterface,都将注入ImplB
  2. Spring环境也应以新值刷新并重新注入到bean中。

我考虑过刷新我的上下文,但那会引起问题。我想也许可以使用setter进行注入,并在重新配置属性后重复使用这些setter。是否有适用于此要求的工作实践呢?

有任何想法吗?

更新

有人建议我可以使用一个工厂/注册表来保存两个实现(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;
        }
}
}

一旦属性更改,我应该重新注入我的环境。有什么建议吗?
我知道我可以在方法内查询该环境,而不是构造函数,但这会降低性能,而且我想考虑重新注入环境的想法(也许使用setter注入?)。

如何将属性值更改为myinterface.type=implB,并且系统如何知道您已完成更改值的操作? - kuhajeyan
1
此外,应用程序应该如何处理已经注入依赖项正在使用且你强制交换实现的情况?此外,这种方法的主要目的是什么? - eg04lt3r
1
@forhas,请查看此链接:https://dev59.com/SWcs5IYBdhLWcg3wgkIQ。这可能对您有所帮助。但是在运行时交换bean实现的方式确实很黑暗。您的问题应该通过正确的设计模式来解决。 - eg04lt3r
1
@forhas,类似问题上有一个很好的答案:https://dev59.com/qHvaa4cB1Zd3GeqPD30N - eg04lt3r
你尝试过Spring插件吗?它自称为“轻量级OSGi”。 - stringy05
显示剩余7条评论
8个回答

32

我希望尽可能保持这个任务的简单性。不是在启动时有条件地加载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接口即可。例如:

然后,针对每个需要被通知实现更改的类,只需让其实现MyInterfaceReloader接口即可。例如:

@Component
public class SomeBean implements MyInterfaceReloader {

    // Do not autowire
    private MyInterface myInterface;

    @Override
    public void changeImplementation(MyInterface impl) {
        this.myInterface = impl;
    }
}

最后,您需要一个能够在每个具有MyInterface属性的bean中实际更改实现的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的常规自动装配规则。

当您想要更改实现时,只需使用新实现的bean名称(即类的小写驼峰式简单名称,例如myImplAmyImplB表示类MyImplAMyImplB)调用updateImplementations方法。

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


看起来这是一个有趣的解决方案。它不使用任何“Spring 魔法”,有些人可能会喜欢这个。至于我,我喜欢 Spring代理解决方案,它使用了所有的“魔法”力量。 - Sergey Bespalov
1
我不会使用这种方法,因为它会使每个客户端都混淆实现切换的概念。这正是动态代理的用例。那么为什么不使用它呢 :-) - Johannes Leimer
1
@JohannesLeimer 嗯,也许说“混乱”有点过了吧?客户端只需要实现 MyInterfaceReloader 接口,该接口仅设置一个属性。动态代理是一个非常好的方法,我同意你的看法。 - fps
1
我们需要那些能够独立思考的人,而不是仅仅死记如何使用现有库的人。谢谢!Federico。至少在这里,我可以用更少的代码示例学习动态代理实现看起来像什么。 - Kanagavelu Sugumar

16

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

希望对您有所帮助。


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

如果是这样,您可以尝试结合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 bean,并且 Spring 上下文不能同时包含这两个实例,则可以尝试使用 ServiceLocatorFactoryBean,并将目标 bean 的作用域设置为 prototype,在目标实现类上使用 Conditional 注释。这种方法可以代替 MyRegistry
P.S. 可能应用程序上下文刷新操作也可以实现您想要的效果,但它可能会导致其他问题,如性能开销。

5
这可能是一个重复的问题或至少非常相似,无论如何我在这里回答了这种类型的问题:Spring bean partial autowire prototype constructor 基本上当您希望运行时使用不同的bean来满足依赖关系时,您需要使用原型范围。然后,您可以使用配置返回原型bean的不同实现。您将需要自己处理要返回哪个实现的逻辑(它们甚至可以返回2个不同的单例bean,这并不重要)。但是假设您想要新的bean,并且返回实现的逻辑在名为SomeBeanWithLogic.isSomeBooleanExpression()的bean中,则可以创建一个配置:
@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的动态行为更改问题绝对是合法的,并且这种设计在面向对象编程中被称为“多态性”。关于你的回答,它与下面另一个答案提出的@Conditional注释非常相似。 - Sergey Bespalov
@SergeyBespalov 不,多态性并不是关于特定对象在运行时表现不同的问题。一个对象只有在其状态改变时才会表现出不同的行为。此外,我的答案与下面的答案不相似,因为上下文不需要重新加载。无论某个引用从一个实现更改为另一个实现,你可能会在那里谈论多态性,但我们不是。我们正在谈论Spring和依赖注入。 - Derrops
我想说的是,单例bean不应该被重新创建。如果由于某种疯狂的原因你想重新创建一个单例bean,那么它应该只有原型作用域,并且你的配置应该处理哪个实现被给出。如果你想在运行时更改单例bean的实现,则更改状态,而不是重新创建它。 - Derrops
2
不,这是关于多个类满足一个接口的问题。只有当特定对象(实例)的状态发生变化时,它的行为才会改变。例如,如果todo已更改或implA或implB中的数据已更改,则Bean.doStuff()将不同。MyInterface可以有不同的实现,这是由于多态性。但doStuff之所以改变是因为状态已经改变了。你很困惑。 - Derrops
我表达了我的观点,让社区决定哪些答案是好的。@forhas 你会加入投票吗? - Sergey Bespalov
显示剩余3条评论

2
您可以使用@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

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

0

1
好的,这是否解决了我在运行时更改注入元素的要求? - forhas
好的,我觉得你可能需要使用这个重新加载上下文。 - Dennis Ich
1
现在你说对了。 - forhas

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接