春天:注入列表,地图,可选对象和getBeansOfType()陷阱

如果您使用Spring框架超过一个星期,那么您可能已经知道此功能。 假设您有多个bean实现了给定的接口。 尝试仅自动接线此类接口的一个bean注定会失败,因为Spring不知道您需要哪个特定实例。 您可以通过使用@Primary批注来指定一个优先于其他实现的“ 最重要 ”实现来解决此问题。 但是在许多合法的用例中,您想注入实现该接口的所有 bean。 例如,您有多个验证器,所有验证器都需要在要同时执行的业务逻辑或几种算法实现之前执行。 自动发现所有的实现在运行时是一个奇妙的例证打开/关闭原理 :您可以轻松地添加新的行为,以业务逻辑(验证,算法,策略-对扩展开放 ),无需触摸的业务逻辑本身(修改关闭 )。

万一我有一个快速的介绍,请随时直接跳到后续章节。 因此,让我们举一个具体的例子。 假设您有一个StringCallable接口和多个实现:

interface StringCallable extends Callable<String> { }
 
@Component
class Third implements StringCallable {
    @Override
    public String call() {
        return "3";
    }
 
}
 
@Component
class Forth implements StringCallable {
    @Override
    public String call() {
        return "4";
    }
 
}
 
@Component
class Fifth implements StringCallable {
    @Override
    public String call() throws Exception {
        return "5";
    }
}

现在,我们可以将List<StringCallable>Set<StringCallable>Map<String, StringCallable>String代表bean名称)注入其他任何类。 为了简化,我将注入一个测试用例:

@SpringBootApplication public class Bootstrap { }
 
@ContextConfiguration(classes = Bootstrap)
class BootstrapTest extends Specification {
 
    @Autowired
    List<StringCallable> list;
 
    @Autowired
    Set<StringCallable> set;
 
    @Autowired
    Map<String, StringCallable> map;
 
    def 'injecting all instances of StringCallable'() {
        expect:
            list.size() == 3
            set.size() == 3
            map.keySet() == ['third', 'forth', 'fifth'].toSet()
    }
 
    def 'enforcing order of injected beans in List'() {
        when:
            def result = list.collect { it.call() }
        then:
            result == ['3', '4', '5']
    }
 
    def 'enforcing order of injected beans in Set'() {
        when:
            def result = set.collect { it.call() }
        then:
            result == ['3', '4', '5']
    }
 
    def 'enforcing order of injected beans in Map'() {
        when:
            def result = map.values().collect { it.call() }
        then:
            result == ['3', '4', '5']
    }
 
}

到目前为止,一切都很好,但是只有第一个测试通过,您能猜出为什么吗?

Condition not satisfied:
 
result == ['3', '4', '5']
|      |
|      false
[3, 5, 4]

毕竟,我们为什么要假设将以与声明bean相同的顺序注入bean? 按字母顺序? 幸运的是,可以使用Ordered接口执行订单:

interface StringCallable extends Callable<String>, Ordered {
}
 
@Component
class Third implements StringCallable {
    //...
 
    @Override public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
 
@Component
class Forth implements StringCallable {
    //...
 
    @Override public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}
 
@Component
class Fifth implements StringCallable {
    //...
 
    @Override public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 2;
    }
}

有趣的是,即使Spring内部注入了LinkedHashMapLinkedHashSet ,也只能正确地排序List 。 我猜它没有记录,也就不足为奇了。 为了结束本介绍,您还可以在Java 8中注入Optional<MyService> ,它按预期方式工作:仅在依赖项可用时注入依赖项。 可选依赖项可能会出现,例如,在广泛使用配置文件时,并且某些配置文件中没有引导某些bean。

处理列表非常麻烦。 大多数情况下,您要遍历它们,以便避免重复,将这样的列表封装在专用包装器中很有用:

@Component
public class Caller {
 
    private final List<StringCallable> callables;
 
    @Autowired
    public Caller(List<StringCallable> callables) {
        this.callables = callables;
    }
 
    public String doWork() {
        return callables.stream()
                .map(StringCallable::call)
                .collect(joining("|"));
    }
 
}

我们的包装器简单地一个接一个地调用所有底层可调用对象,并将它们的结果联接在一起:

@ContextConfiguration(classes = Bootstrap)
class CallerTest extends Specification {
 
    @Autowired
    Caller caller
 
    def 'Caller should invoke all StringCallbles'() {
        when:
            def result = caller.doWork()
        then:
            result == '3|4|5'
    }
 
}

这有点争议,但通常此包装器也实现相同的接口,从而有效地实现复合经典设计模式:

@Component
@Primary
public class Caller implements StringCallable {
 
    private final List<StringCallable> callables;
 
    @Autowired
    public Caller(List<StringCallable> callables) {
        this.callables = callables;
    }
 
    @Override
    public String call() {
        return callables.stream()
                .map(StringCallable::call)
                .collect(joining("|"));
    }
 
}

感谢@Primary我们可以在任何地方简单地自动连接StringCallable ,就好像只有一个bean,而实际上有多个bean一样,我们可以注入Composite。 在重构旧应用程序时,这很有用,因为它保留了向后兼容性。

为什么我甚至从所有这些基础开始? 如果你仔细关系十分密切,代码片段上面介绍鸡和蛋的问题:实例StringCallable需要的所有实例StringCallable ,所以从技术上来说callables列表应该包括Caller为好。 但是Caller当前正在创建中,所以这是不可能的。 这很有道理,幸运的是Spring意识到了这种特殊情况。 但是在更高级的情况下,这可能会咬你。 后来,新的开发人员介绍了这一点

@Component
public class EnterpriseyManagerFactoryProxyHelperDispatcher {
 
    private final Caller caller;
 
    @Autowired
    public EnterpriseyManagerFactoryProxyHelperDispatcher(Caller caller) {
        this.caller = caller;
    }
}

到目前为止,除了类名,其他都没错。 但是,如果其中一个StringCallables对此有依赖关系会怎样?

@Component
class Fifth implements StringCallable {
 
    private final EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher;
 
    @Autowired
    public Fifth(EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }
 
}

现在,我们创建了一个循环依赖关系,并且由于我们通过构造函数进行注入(这一直是我们的本意),因此Spring在启动时会一巴掌:

UnsatisfiedDependencyException:
    Error creating bean with name 'caller' defined in file ...
UnsatisfiedDependencyException: 
    Error creating bean with name 'fifth' defined in file ...
UnsatisfiedDependencyException: 
    Error creating bean with name 'enterpriseyManagerFactoryProxyHelperDispatcher' defined in file ...
BeanCurrentlyInCreationException: 
    Error creating bean with name 'caller': Requested bean is currently in creation: 
        Is there an unresolvable circular reference?

和我在一起,我在这里建立高潮。 显然,这是一个错误,很遗憾可以通过字段注入(或与此有关的设置)来解决:

@Component
public class Caller {
 
    @Autowired
    private List<StringCallable> callables;
 
    public String doWork() {
        return callables.stream()
                .map(StringCallable::call)
                .collect(joining("|"));
    }
 
}

通过将注入的Bean创建与耦合分离(构造函数注入是不可能的),我们现在可以创建一个循环依赖图,其中Caller持有一个引用Enterprisey...Fifth类的实例,该实例又反过来引用了同一Caller实例。 依赖关系图中的循环是一种设计气味,导致无法维持意大利面条关系图。 请避免使用它们,并且如果构造函数注入可以完全阻止它们,那就更好了。

会议

有趣的是,还有另一种直接适用于Spring的解决方案:

ListableBeanFactory.getBeansOfType()

@Component
public class Caller {
 
    private final List<StringCallable> callables;
 
    @Autowired
    public Caller(ListableBeanFactory beanFactory) {
        callables = new ArrayList<>(beanFactory.getBeansOfType(StringCallable.class).values());
    }
 
    public String doWork() {
        return callables.stream()
                .map(StringCallable::call)
                .collect(joining("|"));
    }
 
}

问题解决了? 恰恰相反! getBeansOfType()会静默跳过正在创建的bean(嗯,有TRACEDEBUG日志…),并且仅返回那些已经存在的bean。 因此,刚刚创建了Caller并成功启动了容器,而它不再引用Fifth bean。 您可能会说我要这样,因为我们有一个循环依赖关系,所以会发生奇怪的事情。 但这是getBeansOfType()的固有功能。 为了理解为什么在容器启动过程中使用getBeansOfType()是个坏主意 ,请查看以下情形(省略了不重要的代码):

@Component
class Alpha {
 
    static { log.info("Class loaded"); }
 
    @Autowired
    public Alpha(ListableBeanFactory beanFactory) {
        log.info("Constructor");
        log.info("Constructor (beta?):  {}", beanFactory.getBeansOfType(Beta.class).keySet());
        log.info("Constructor (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet());
    }
 
    @PostConstruct
    public void init() {
        log.info("@PostConstruct (beta?):  {}", beanFactory.getBeansOfType(Beta.class).keySet());
        log.info("@PostConstruct (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet());
    }
 
}
 
@Component
class Beta {
 
    static { log.info("Class loaded"); }
 
    @Autowired
    public Beta(ListableBeanFactory beanFactory) {
        log.info("Constructor");
        log.info("Constructor (alpha?): {}", beanFactory.getBeansOfType(Alpha.class).keySet());
        log.info("Constructor (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet());
    }
 
    @PostConstruct
    public void init() {
        log.info("@PostConstruct (alpha?): {}", beanFactory.getBeansOfType(Alpha.class).keySet());
        log.info("@PostConstruct (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet());
    }
 
}
 
@Component
class Gamma {
 
    static { log.info("Class loaded"); }
 
    public Gamma() {
        log.info("Constructor");
    }
 
    @PostConstruct
    public void init() {
        log.info("@PostConstruct");
    }
}

日志输出显示了Spring如何在内部加载和解析类:

Alpha: | Class loaded
Alpha: | Constructor
Beta:  | Class loaded
Beta:  | Constructor
Beta:  | Constructor (alpha?): []
Gamma: | Class loaded
Gamma: | Constructor
Gamma: | @PostConstruct
Beta:  | Constructor (gamma?): [gamma]
Beta:  | @PostConstruct (alpha?): []
Beta:  | @PostConstruct (gamma?): [gamma]
Alpha: | Constructor (beta?):  [beta]
Alpha: | Constructor (gamma?): [gamma]
Alpha: | @PostConstruct (beta?):  [beta]
Alpha: | @PostConstruct (gamma?): [gamma]

Spring框架首先加载Alpha并尝试实例化bean。 但是,在运行getBeansOfType(Beta.class)它会发现Beta因此将继续加载并实例化该Beta 。 在Beta内部,我们可以立即发现问题:当Beta询问beanFactory.getBeansOfType(Alpha.class)时,不会得到任何结果( [] )。 Spring将默默地忽略Alpha ,因为它正在创建中。 后来一切都按预期进行: Gamma已加载,构造和注入, Beta看到了Gamma ,当我们返回Alpha ,一切就绪。 请注意,即使将getBeansOfType()移至@PostConstruct方法也无济于事–在实例化所有bean时,最终不会执行这些回调–而是在容器启动时。

意见建议

很少需要getBeansOfType() ,如果您具有循环依赖性,那么结果是不可预测的。 当然,您首先应该避免使用它们,如果您通过集合正确注入了依赖关系,Spring可以预见地处理所有bean的生命周期,并正确地连接它们或在运行时失败。 在bean之间存在循环依赖关系时(有时是偶然的,或者在依赖关系图中的节点和边方面很长), getBeansOfType()会根据我们无法控制的因素(例如CLASSPATH顺序getBeansOfType()产生不同的结果。

PS:JakubKubryński进行getBeansOfType()疑难解答表示getBeansOfType()

翻译自: https://www.javacodegeeks.com/2015/04/spring-injecting-lists-maps-optionals-and-getbeansoftype-pitfalls.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值