【问题标题】:How to dynamically inject a service using a runtime "qualifier" variable?如何使用运行时“限定符”变量动态注入服务?
【发布时间】:2017-08-12 01:35:51
【问题描述】:

在给定运行时值的情况下,我找不到注入组件/服务的简单方法。

我开始阅读@Spring的文档:http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-autowired-annotation-qualifiers 但我找不到如何改变传递给 @Qualifier 注释的值。

假设我有一个具有这种接口的模型实体:

public interface Case {

    String getCountryCode();
    void setCountryCode(String countryCode);

}

在我的客户端代码中,我会执行以下操作:

@Inject
DoService does;

(...)

Case myCase = new CaseImpl(); // ...or whatever
myCase.setCountryCode("uk");

does.whateverWith(myCase);

...我的服务是:

@Service
public class DoService {

    @Inject
    // FIXME what kind of #$@& symbol can I use here?
    // Seems like SpEL is sadly invalid here :(
    @Qualifier("${caze.countryCode}")
    private CaseService caseService;

    public void whateverWith(Case caze) {
        caseService.modify(caze);
    }

}

我希望 caseService 是 UKCaseService(请参阅下面的相关代码)。

public interface CaseService {

    void modify(Case caze);

}

@Service
@Qualifier("uk")
public class UKCaseService implements CaseService {

}

@Service
@Qualifier("us")
public class USCaseService implements CaseService {

}

那么我如何以最简单/优雅/有效的方式通过使用任何一个/所有 Spring 特性来“修复”所有这些,所以基本上没有 .properties,没有 XML,只有注释。 但是我已经怀疑我的 DoService 有问题,因为 Spring 在注入 caseService 之前需要知道“案例”......但是如何在客户端代码不知道 caseService 的情况下实现这一点?! 我想不通...

我已经在这里阅读了几个关于 SO 的问题,但大多数时候,要么他们的需求和/或配置与我不同,要么发布的答案对我来说不够满意(看起来他们'本质上是变通办法或(旧)使用(旧)Spring特性)。

How does Spring autowire by name when more than one matching bean is found? => 仅指类组件类

Dynamically defining which bean to autowire in Spring (using qualifiers) => 真的很有趣,但最详尽的答案(4 票)是......几乎 3 1/2 岁?! (2013 年 7 月)

Spring 3 - Dynamic Autowiring at runtime based on another object attribute =>这里的问题非常相似,但答案看起来真的是一种解决方法而不是真正的设计模式(如工厂)?而且我不喜欢将所有代码都实现到 ServiceImpl 中...

Spring @Autowiring, how to use an object factory to choose implementation? => 第二个答案似乎很有趣,但它的作者没有扩展,所以虽然我(有点)了解 Java Config 和东西,但我不太确定他在说什么......

How to inject different services at runtime based on a property with Spring without XML => 有趣的讨论,尤其是。答案,但用户设置了我没有的属性。

另请阅读: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-bean-references => 我找不到关于在表达式中使用“@”的扩展示例。有人知道吗?

编辑: 发现了其他相关的类似问题,没有人得到正确的答案: How to use @Autowired to dynamically inject implementation like a factory pattern Spring Qualifier and property placeholder Spring: Using @Qualifier with Property Placeholder How to do conditional auto-wiring in Spring? Dynamic injection in Spring SpEL in @Qualifier refer to same bean How to use SpEL to inject result of method call in Spring?

工厂模式可能是一个解决方案? How to use @Autowired to dynamically inject implementation like a factory pattern

【问题讨论】:

标签: spring service dependency-injection autowired


【解决方案1】:

您可以使用 BeanFactory 动态地从上下文中获取您的 bean:

@Service
public class Doer {

  @Autowired BeanFactory beans;

  public void doSomething(Case case){
    CaseService service = beans.getBean(case.getCountryCode(), CaseService.class)
    service.doSomething(case);
  }
}

附注。 使用国家代码作为 bean 名称看起来有点奇怪。至少添加一些前缀或更好地考虑其他一些设计模式。

如果您仍然希望每个国家/地区都有 bean,我会建议另一种方法。引入注册服务,按国家代码获取所需服务:

@Service
public class CaseServices {

  private final Map<String, CaseService> servicesByCountryCode = new HashMap<>();

  @Autowired
  public CaseServices(List<CaseService> services){
    for (CaseService service: services){
      register(service.getCountryCode(), service);
    }
  }

  public void register(String countryCode, CaseService service) {
    this.servicesByCountryCode.put(countryCode, service);
  }

  public CaseService getCaseService(String countryCode){
    return this.servicesByCountryCode.get(countryCode);
  }
}

示例用法:

@Service
public class DoService {

  @Autowired CaseServices caseServices;

  public void doSomethingWith(Case case){
    CaseService service = caseServices.getCaseService(case.getCountryCode());
    service.modify(case);
  }
}

在这种情况下,您必须将 String getCountryCode() 方法添加到您的 CaseService 接口。

public interface CaseService {
    void modify(Case case);
    String getCountryCode();
}

或者,您可以添加方法CaseService.supports(Case case) 来选择服务。或者,如果您无法扩展接口,您可以从某个初始化程序或 @Configuration 类中调用 CaseServices.register(String, CaseService) 方法。

更新:忘了提一下,Spring 已经提供了一个很好的 Plugin 抽象来重用样板代码来创建这样的 PluginRegistry

例子:

public interface CaseService extends Plugin<String>{
    void doSomething(Case case);
}

@Service
@Priority(0)
public class SwissCaseService implements CaseService {

  void doSomething(Case case){
    // Do something with the Swiss case
  }

  boolean supports(String countryCode){
    return countryCode.equals("CH");
  }
}

@Service
@Priority(Ordered.LOWEST_PRECEDENCE)
public class DefaultCaseService implements CaseService {

  void doSomething(Case case){
    // Do something with the case by-default
  }

  boolean supports(String countryCode){
    return true;
  }
}

@Service
public class CaseServices {

  private final PluginRegistry<CaseService<?>, String> registry;

  @Autowired
  public Cases(List<CaseService> services){
    this.registry = OrderAwarePluginRegistry.create(services);
  }

  public CaseService getCaseService(String countryCode){
    return registry.getPluginFor(countryCode);
  }
}

【讨论】:

  • 其实不是“国家代码作为bean名称”,而是国家代码作为限定符值。 ;) 但最后,正如我上面提到的工厂模式作为结论,我开始开发类似的东西。
  • 很高兴您找到了解决方案。我仍然认为注册表模式对于这种情况可能更可取,尽管我知道您的实际情况可能会有所不同,并且没有您描述的那么简单。祝你好运+
  • 我认为第一个例子有点误导,因为 @Autowired BeanFactory beans; 在没有更多代码的情况下无法开箱即用,因为它将返回 null
  • 我发现注册表解决方案非常优雅,我唯一的问题是,public CaseServices(List&lt;CaseService&gt; services) 服务是如何自动连接的? Spring 框架是否会扫描所有实现 CaseInterface 的可用 bean 并将其注入到 CaseServices 中?
  • 不用回答,我已经测试过了,效果很好。干得好老兄!!!
【解决方案2】:

根据这个 SO 答案,使用 @Qualifier 对您没有多大帮助:Get bean from ApplicationContext by qualifier

至于替代策略:

  • 如果你是 spring boot,你可以使用@ConditonalOnProperty 或另一个Conditional

  • @aux 建议的查找服务

  • 只需一致地命名您的 bean 并在运行时按名称查找它们。

请注意,您的用例似乎也围绕在应用程序启动时创建 bean 的场景展开,但选择的 bean 需要在 applicationContext 完成注入 bean 之后解析。

【讨论】:

  • 我认为你必须小心你的工厂模式。
  • 您使用myCase.setCountryCode("uk"); 的意图不是线程安全的,除非您使用ThreadLocal 来存储CaseService 实例。您可能可以使用ObjectProvider&lt;T&gt;#getObject(Object... args) 在您的设置器中设置 ThreadLocal。
  • 我在调用setCountryCode() 时没有设置CaseService。这是对象的初始化。为了清楚起见,我进行了编辑。
猜你喜欢
  • 1970-01-01
  • 2017-08-31
  • 1970-01-01
  • 2015-06-27
  • 2023-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多