[英]Inject spring bean dynamically
在 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实现了这一点)。
我希望在运行时 - 一旦属性更改,将发生以下情况(无需重新启动服务器):
myinterface.type=implB
ImplB 将被注入到任何使用 MyInterface 的地方我想刷新我的上下文,但这会产生问题。 我想也许可以使用 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;
}
}
}
一旦属性发生变化,我应该重新注入我的环境。 对此有何建议?
我知道我可以在方法中查询 env 而不是构造函数,但这会降低性能,而且我想考虑一个用于重新注入环境的 ider(同样,也许使用 setter 注入?)。
我会让这个任务尽可能简单。 与其在启动时有条件地加载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
或myImplB
上课MyImplA
和MyImplB
。
您还应该在启动时调用此方法,以便在每个实现MyInterfaceReloader
接口的 bean 上设置初始实现。
我通过使用 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();
}
}
希望它有帮助。
如果我理解正确,那么目标不是替换注入的对象实例,而是在接口方法调用期间使用不同的实现取决于运行时的某些条件。
如果是这样,那么您可以尝试结合ProxyFactoryBean来查看Sring
TargetSource机制。 关键是代理对象将被注入到使用你的接口的 bean 中,并且所有接口方法调用将被发送到TargetSource
目标。
我们称之为“多态代理”。
看看下面的例子:
条件目标源.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.
}
}
应用上下文.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与prototype
目标 bean 范围和目标实现类上的Conditional
注释。 这种方法可以用来代替MyRegistry
。
PS也许应用程序上下文刷新操作也可以做你想做的,但它可能会导致其他问题,如性能开销。
这可能是一个重复的问题或至少非常相似,无论如何我在这里回答了这类问题: Spring bean 部分自动装配原型构造函数
几乎当您需要在运行时为依赖项使用不同的 bean 时,您需要使用原型作用域。 然后你可以使用一个配置来返回原型 bean 的不同实现。 您将需要处理自己返回哪个实现的逻辑,(它们甚至可能返回 2 个不同的单例 bean,这无关紧要)但是假设您想要新 bean,并且返回实现的逻辑在一个名为SomeBeanWithLogic.isSomeBooleanExpression()
的 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 被用在一个单例 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();
}
}
}
请注意 - 如果有趣的话 - FileChangedReloadingStrategy 使您的项目高度依赖于部署条件:WAR/EAR 应该由容器分解,并且您应该可以直接访问文件系统,这些条件并非总是在所有情况下都满足和环境。
您可以使用 @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;
}
然后将属性文件中的实现类型设置为 -
myinterface.type=implA
您可以在属性值上使用 Spring @Conditional。 为两个 Bean 提供相同的名称,它应该可以正常工作,因为只会创建一个实例。
在此处查看如何在服务和组件上使用 @Conditional: http : //blog.codeleak.pl/2015/11/how-to-register-components-using.html
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.