春天猫rtsy
如果您使用Spring框架超过一个星期,那么您可能已经知道此功能。 假设您有多个bean实现了给定的接口。 尝试仅自动连接此类接口的一个bean注定会失败,因为Spring不知道您需要哪个特定实例。 您可以通过使用@Primary
批注来指定一个优先于其他实现的“ 最重要 ”实现,从而解决此问题。 但是在许多合法的用例中,您想注入所有实现了上述接口的bean。 例如,您有多个验证器,所有验证器都需要在业务逻辑或要同时执行的几种算法实现之前执行。 自动发现所有的实现在运行时是一个奇妙的例证打开/关闭原理 :您可以轻松地添加新的行为,以业务逻辑(验证,算法,策略-对扩展开放 ),无需触摸的业务逻辑本身(修改关闭 )。
万一我有一个快速的介绍,请随时直接跳到后续章节。 因此,让我们举一个具体的例子。 假设您有一个StringCallable
接口和多个实现:
interface StringCallable extends Callable<String> { }@Component
class Third implements StringCallable {@Overridepublic String call() {return "3";}}@Component
class Forth implements StringCallable {@Overridepublic String call() {return "4";}}@Component
class Fifth implements StringCallable {@Overridepublic 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 {@AutowiredList<StringCallable> list;@AutowiredSet<StringCallable> set;@AutowiredMap<String, StringCallable> map;def 'injecting all instances of StringCallable'() {expect:list.size() == 3set.size() == 3map.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内部注入了LinkedHashMap
和LinkedHashSet
,也仅对List
进行了正确排序。 我猜它没有记录,也就不足为奇了。 为了结束本介绍,您还可以在Java 8中注入Optional<MyService>
,它按预期方式工作:仅在依赖项可用时注入依赖项。 可选依赖项可能会出现,例如,在广泛使用概要文件时,并且某些概要文件中没有引导某些bean。
处理列表非常麻烦。 大多数情况下,您要遍历它们,因此为了避免重复,将这样的列表封装在专用包装器中很有用:
@Component
public class Caller {private final List<StringCallable> callables;@Autowiredpublic 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 {@AutowiredCaller callerdef '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;@Autowiredpublic Caller(List<StringCallable> callables) {this.callables = callables;}@Overridepublic 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;@Autowiredpublic EnterpriseyManagerFactoryProxyHelperDispatcher(Caller caller) {this.caller = caller;}
}
到目前为止,除了类名,其他都没错。 但是,如果其中一个StringCallables
对此有依赖关系会怎样?
@Component
class Fifth implements StringCallable {private final EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher;@Autowiredpublic 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 {@Autowiredprivate List<StringCallable> callables;public String doWork() {return callables.stream().map(StringCallable::call).collect(joining("|"));}}
现在,通过将注入的bean创建与耦合分离(构造函数注入是不可能的),我们现在可以创建一个循环依赖图,其中Caller
持有一个引用Enterprisey...
的Fifth
类的实例,该实例又反过来引用了同一Caller
实例。 依存关系图中的循环是一种设计气味,导致无法维持意大利面条关系图。 请避免使用它们,如果构造函数注入可以完全阻止它们,那就更好了。
会议
有趣的是,还有另一种直接适用于Spring guts的解决方案:
ListableBeanFactory.getBeansOfType()
:
@Component
public class Caller {private final List<StringCallable> callables;@Autowiredpublic Caller(ListableBeanFactory beanFactory) {callables = new ArrayList<>(beanFactory.getBeansOfType(StringCallable.class).values());}public String doWork() {return callables.stream().map(StringCallable::call).collect(joining("|"));}}
问题解决了? 恰恰相反! getBeansOfType()
将在创建过程中静默跳过(嗯,有TRACE
和DEBUG
日志…)bean,并且仅返回那些已经存在的bean。 因此,刚刚创建了Caller
并成功启动了容器,而它不再引用Fifth
bean。 您可能会说我要这样,因为我们有一个循环依赖关系,所以会发生奇怪的事情。 但这是getBeansOfType()
的固有功能。 为了了解为什么在容器启动过程中使用getBeansOfType()
是一个坏主意 ,请查看以下情形(省略了不重要的代码):
@Component
class Alpha {static { log.info("Class loaded"); }@Autowiredpublic Alpha(ListableBeanFactory beanFactory) {log.info("Constructor");log.info("Constructor (beta?): {}", beanFactory.getBeansOfType(Beta.class).keySet());log.info("Constructor (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet());}@PostConstructpublic 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"); }@Autowiredpublic Beta(ListableBeanFactory beanFactory) {log.info("Constructor");log.info("Constructor (alpha?): {}", beanFactory.getBeansOfType(Alpha.class).keySet());log.info("Constructor (gamma?): {}", beanFactory.getBeansOfType(Gamma.class).keySet());}@PostConstructpublic 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");}@PostConstructpublic 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
春天猫rtsy