春天猫rtsy_春天:注入列表,地图,可选对象和getBeansOfType()陷阱
春天貓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
總結
以上是生活随笔為你收集整理的春天猫rtsy_春天:注入列表,地图,可选对象和getBeansOfType()陷阱的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jax-rs jax-ws_如何以大数据
- 下一篇: 打七折怎么算 打七折的计算方式