春天:注入列表,地图,可选对象和getBeansOfType()陷阱
如果您使用Spring框架超過(guò)一個(gè)星期,那么您可能已經(jīng)知道此功能。 假設(shè)您有多個(gè)bean實(shí)現(xiàn)了給定的接口。 嘗試僅自動(dòng)接線此類(lèi)接口的一個(gè)bean注定會(huì)失敗,因?yàn)镾pring不知道您需要哪個(gè)特定實(shí)例。 您可以通過(guò)使用@Primary批注來(lái)指定一個(gè)優(yōu)先于其他實(shí)現(xiàn)的“ 最重要 ”實(shí)現(xiàn)來(lái)解決此問(wèn)題。 但是在許多合法的用例中,您想注入實(shí)現(xiàn)該接口的所有 bean。 例如,您有多個(gè)驗(yàn)證器,所有驗(yàn)證器都需要在要同時(shí)執(zhí)行的業(yè)務(wù)邏輯或幾種算法實(shí)現(xiàn)之前執(zhí)行。 自動(dòng)發(fā)現(xiàn)所有的實(shí)現(xiàn)在運(yùn)行時(shí)是一個(gè)奇妙的例證打開(kāi)/關(guān)閉原理 :您可以輕松地添加新的行為,以業(yè)務(wù)邏輯(驗(yàn)證,算法,策略-對(duì)擴(kuò)展開(kāi)放 ),無(wú)需觸摸的業(yè)務(wù)邏輯本身(修改關(guān)閉 )。
萬(wàn)一我有一個(gè)快速的介紹,請(qǐng)隨時(shí)直接跳到后續(xù)章節(jié)。 因此,讓我們舉一個(gè)具體的例子。 假設(shè)您有一個(gè)StringCallable接口和多個(gè)實(shí)現(xiàn):
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";} }現(xiàn)在,我們可以將List<StringCallable> , Set<StringCallable>或Map<String, StringCallable> ( String代表bean名稱(chēng))注入其他任何類(lèi)。 為了簡(jiǎn)化,我將注入一個(gè)測(cè)試用例:
@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']}}到目前為止,一切都很好,但是只有第一個(gè)測(cè)試通過(guò),您能猜出為什么嗎?
Condition not satisfied:result == ['3', '4', '5'] | | | false [3, 5, 4]畢竟,我們?yōu)槭裁匆僭O(shè)將以與聲明bean相同的順序注入bean? 按字母順序? 幸運(yùn)的是,可以使用Ordered接口執(zhí)行訂單:
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內(nèi)部注入了LinkedHashMap和LinkedHashSet ,也只能正確地排序List 。 我猜它沒(méi)有記錄,也就不足為奇了。 為了結(jié)束本介紹,您還可以在Java 8中注入Optional<MyService> ,它按預(yù)期方式工作:僅在依賴(lài)項(xiàng)可用時(shí)注入依賴(lài)項(xiàng)。 可選依賴(lài)項(xiàng)可能會(huì)出現(xiàn),例如,在廣泛使用配置文件時(shí),并且某些配置文件中沒(méi)有引導(dǎo)某些bean。
處理列表非常麻煩。 大多數(shù)情況下,您要遍歷它們,以便避免重復(fù),將這樣的列表封裝在專(zhuān)用包裝器中很有用:
@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("|"));}}我們的包裝器簡(jiǎn)單地一個(gè)接一個(gè)地調(diào)用所有底層可調(diào)用對(duì)象,并將它們的結(jié)果聯(lián)接在一起:
@ContextConfiguration(classes = Bootstrap) class CallerTest extends Specification {@AutowiredCaller callerdef 'Caller should invoke all StringCallbles'() {when:def result = caller.doWork()then:result == '3|4|5'}}這有點(diǎn)爭(zhēng)議,但通常此包裝器也實(shí)現(xiàn)相同的接口,從而有效地實(shí)現(xiàn)復(fù)合經(jīng)典設(shè)計(jì)模式:
@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我們可以在任何地方簡(jiǎn)單地自動(dòng)連接StringCallable ,就好像只有一個(gè)bean,而實(shí)際上有多個(gè)bean一樣,我們可以注入Composite。 在重構(gòu)舊應(yīng)用程序時(shí),這很有用,因?yàn)樗A袅讼蚝蠹嫒菪浴?
為什么我甚至從所有這些基礎(chǔ)開(kāi)始? 如果你仔細(xì)關(guān)系十分密切,代碼片段上面介紹雞和蛋的問(wèn)題:實(shí)例StringCallable需要的所有實(shí)例StringCallable ,所以從技術(shù)上來(lái)說(shuō)callables列表應(yīng)該包括Caller為好。 但是Caller當(dāng)前正在創(chuàng)建中,所以這是不可能的。 這很有道理,幸運(yùn)的是Spring意識(shí)到了這種特殊情況。 但是在更高級(jí)的情況下,這可能會(huì)咬你。 后來(lái),新的開(kāi)發(fā)人員介紹了這一點(diǎn) :
@Component public class EnterpriseyManagerFactoryProxyHelperDispatcher {private final Caller caller;@Autowiredpublic EnterpriseyManagerFactoryProxyHelperDispatcher(Caller caller) {this.caller = caller;} }到目前為止,除了類(lèi)名,其他都沒(méi)錯(cuò)。 但是,如果其中一個(gè)StringCallables對(duì)此有依賴(lài)關(guān)系會(huì)怎樣?
@Component class Fifth implements StringCallable {private final EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher;@Autowiredpublic Fifth(EnterpriseyManagerFactoryProxyHelperDispatcher dispatcher) {this.dispatcher = dispatcher;}}現(xiàn)在,我們創(chuàng)建了一個(gè)循環(huán)依賴(lài)關(guān)系,并且由于我們通過(guò)構(gòu)造函數(shù)進(jìn)行注入(這一直是我們的本意),因此Spring在啟動(dòng)時(shí)會(huì)一巴掌:
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?和我在一起,我在這里建立高潮。 顯然,這是一個(gè)錯(cuò)誤,很遺憾可以通過(guò)字段注入(或與此有關(guān)的設(shè)置)來(lái)解決:
@Component public class Caller {@Autowiredprivate List<StringCallable> callables;public String doWork() {return callables.stream().map(StringCallable::call).collect(joining("|"));}}通過(guò)將注入的Bean創(chuàng)建與耦合分離(構(gòu)造函數(shù)注入是不可能的),我們現(xiàn)在可以創(chuàng)建一個(gè)循環(huán)依賴(lài)圖,其中Caller持有Fifth類(lèi)的一個(gè)實(shí)例,該實(shí)例引用Enterprisey... ,而后者又又引用了同一Caller實(shí)例。 依賴(lài)關(guān)系圖中的循環(huán)是一種設(shè)計(jì)氣味,導(dǎo)致無(wú)法維持意大利面條關(guān)系圖。 請(qǐng)避免使用它們,并且如果構(gòu)造函數(shù)注入可以完全阻止它們,那就更好了。
會(huì)議
有趣的是,還有另一種直接適用于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("|"));}}問(wèn)題解決了? 恰恰相反! getBeansOfType()會(huì)靜默跳過(guò)正在創(chuàng)建的bean(嗯,有TRACE和DEBUG日志…),并且僅返回那些已經(jīng)存在的bean。 因此,剛剛創(chuàng)建了Caller并成功啟動(dòng)了容器,而它不再引用Fifth bean。 您可能會(huì)說(shuō)我要這樣,因?yàn)槲覀冇幸粋€(gè)循環(huán)依賴(lài)關(guān)系,所以會(huì)發(fā)生奇怪的事情。 但這是getBeansOfType()的固有功能。 為了理解為什么在容器啟動(dòng)過(guò)程中使用getBeansOfType()是個(gè)壞主意 ,請(qǐng)查看以下情形(省略了不重要的代碼):
@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如何在內(nèi)部加載和解析類(lèi):
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并嘗試實(shí)例化bean。 但是,在運(yùn)行g(shù)etBeansOfType(Beta.class)它會(huì)發(fā)現(xiàn)Beta因此將繼續(xù)加載并實(shí)例化該Beta 。 在Beta內(nèi)部,我們可以立即發(fā)現(xiàn)問(wèn)題:當(dāng)Beta詢(xún)問(wèn)beanFactory.getBeansOfType(Alpha.class)時(shí),不會(huì)得到任何結(jié)果( [] )。 Spring將默默地忽略Alpha ,因?yàn)樗趧?chuàng)建中。 后來(lái)一切都按預(yù)期進(jìn)行: Gamma已加載,構(gòu)造和注入, Beta看到了Gamma ,當(dāng)我們返回Alpha ,一切就緒。 請(qǐng)注意,即使將getBeansOfType()移至@PostConstruct方法也無(wú)濟(jì)于事–在實(shí)例化所有bean時(shí),最終不會(huì)執(zhí)行這些回調(diào)–而是在容器啟動(dòng)時(shí)。
意見(jiàn)建議
很少需要getBeansOfType() ,如果您具有循環(huán)依賴(lài)性,那么結(jié)果是不可預(yù)測(cè)的。 當(dāng)然,您首先應(yīng)該避免使用它們,如果您通過(guò)集合正確注入了依賴(lài)關(guān)系,Spring可以預(yù)見(jiàn)地處理所有bean的生命周期,并正確地連接它們或在運(yùn)行時(shí)失敗。 在bean之間存在循環(huán)依賴(lài)關(guān)系時(shí)(有時(shí)是偶然的,或者在依賴(lài)關(guān)系圖中的節(jié)點(diǎn)和邊方面很長(zhǎng)), getBeansOfType()會(huì)根據(jù)我們無(wú)法控制的因素(例如CLASSPATH順序getBeansOfType()產(chǎn)生不同的結(jié)果。
PS:對(duì)JakubKubryński進(jìn)行g(shù)etBeansOfType()疑難解答表示getBeansOfType() 。
翻譯自: https://www.javacodegeeks.com/2015/04/spring-injecting-lists-maps-optionals-and-getbeansoftype-pitfalls.html
總結(jié)
以上是生活随笔為你收集整理的春天:注入列表,地图,可选对象和getBeansOfType()陷阱的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 华为Mate X5折叠屏新品开箱轻体验:
- 下一篇: 凯华发布全新快触发 CP 轴:罗密欧轴与