用實戰(zhàn)》第 4 章 IOC 容器中的 Bean 閱讀筆記 6)
《SpringBoot 3入門與應(yīng)用實戰(zhàn)》第 4 章 IOC 容器中的 Bean 閱讀筆記 6Spring Framework 中的 IOC 容器內(nèi)部設(shè)計極其精巧提供了強大且實用的功能和機制通過加載解析 XML 配置文件、注解配置類實現(xiàn)按照配置內(nèi)容工作。4.1 BeanFactory第2章講到BeanFactory 是基礎(chǔ) IOC 容器ApplicationContext 包含 BeanFactory 的所有功能且擴展了更多實用特性。BeanFactory 作為Spring Framework 中的基礎(chǔ) IOC 容器它的設(shè)計相對簡單且純粹但也是最核心的。4.1.1 BeanFactory 接口系列作為 Spring Framework 中的頂級容器接口BeanFactory 的設(shè)計是簡單純粹的。下面將對 BeanFactory 的核心特性進行分析。1.核心特性BeanFactory 接口直接擴展的 3 個子接口可以總結(jié)出 BeanFactory 的特性層次性、可列舉、支持自動注入。層次性BeanFactory本身支持父子結(jié)構(gòu)如果在一個BeanFactory中沒有找到想要的Bean則會向上查找父BeanFactory。可列舉ListableBeanFactory 支持將容器中所有的 Bean一一列舉?。支持自動注入BeanFactory 中定義了 getBean 方法用于依賴查找其子接口 AutowireCapableBeanFactory 中對依賴注入予以支持。2.可讀與可寫在 HierarchicalBeanFactory 的下方有一個接口ConfigurableBeanFactory。Spring Framework 中對于核心 API 的命名有非常強的規(guī)律性當(dāng)我們看到類名的前面帶有 “Configurable” 時意味著這個接口的行為有“寫”的動作不帶 Configurable 前綴的接口只有“讀”的動作。可寫與可讀的概念在學(xué)習(xí) Java SE 的面向?qū)ο缶幊讨幸粋€類的屬性設(shè)置為 private 后提供 getter 方法則意味著該屬性可讀提供 setter 方法則意味著該屬性可寫。同樣Spring Framework 中的這些 BeanFactory包括后面的 ApplicationContext 中都有這樣的設(shè)計普通的 BeanFactory 只有 get 相關(guān)的操作而 “Configurable” 開頭的 BeanFactory 或者 ApplicationContext 就具有了 set 相關(guān)的操作。4.1.2 BeanFactory 的實現(xiàn)類有關(guān) BeanFactory 實現(xiàn)類的簡單講解我們重點關(guān)注兩個抽象子類和一個落地實現(xiàn)類。抽象實現(xiàn) AbstractBeanFactory、一個底層實現(xiàn)DefaultListableBeanFactory 以及介于兩者之間的 AbstractAutowireCapableBeanFactory。1.核心特性簡單描述 BeanFactory 的實現(xiàn)類中設(shè)計的功能和機制。加載 Bean 的信息BeanFactory 利用不同的加載機制XML或注解配置類等加載 Bean 的信息。作用域BeanFactory 的內(nèi)部能分辨 Bean 的作用域是單實例還是原型等。創(chuàng)建 Bean BeanFactory 具備創(chuàng)建 Bean 的能力以及屬性賦值、依賴注入、生命周期回調(diào)。生命周期管理BeanFactory 負責(zé)控制 Bean 在創(chuàng)建階段的生命周期。2.BeanFactory 的約定在 BeanFactory 的設(shè)計之初它制定了 Bean 創(chuàng)建的兩大環(huán)節(jié)和幾個核心步驟。(1)加載 Bean 的注冊信息從 XML 配置文件、注解配置類、模式注解組件掃描等方式得知哪些 Bean 需要被注入到 IOC 容器。(2)創(chuàng)建并初始化 Bean該環(huán)節(jié)包含 2 個步驟分別是 Bean 的創(chuàng)建、Bean的初始化。4.2 ApplicationContextBeanFactory 僅實現(xiàn)了 IOC 容器中最核心的容器部分而 ApplicationContext 提供了更多可以用于實際應(yīng)用開發(fā)的強大功能特性。打一個比方如果說 BeanFactory 是 IOC 容器的“芯片”?那么 ApplicationContext 則是一臺完整的主機。4.2.1 ApplicationContext 接口系列借助 IDEA可以展示 ApplicationContext 接口的父接口、擴展接口之間的關(guān)系如圖所示。從圖中可以看到ApplicationContext 除了繼承 BeanFactory 接口還額外繼承了幾個功能性接口這些接口共同組成了 ApplicationContext 擴展的幾個核心特性。簡單概括 ApplicationContext 的功能如下。訪問 Bean 的能力ApplicationContext 繼承自 BeanFactory擁有 BeanFactory 的所有能力。層級關(guān)系A(chǔ)pplicationContext與BeanFactory一樣也存在層級關(guān)系。加載文件資源ApplicationContext 可以從類路徑 (classpath)、文件系統(tǒng)等來源中加載文件。事件發(fā)布與驅(qū)動機制ApplicationContext 設(shè)計了基于觀察者模式的事件驅(qū)動機制可以實現(xiàn)事件的發(fā)布與廣播。國際化支持ApplicationContext可以針對不同國家/地區(qū)的訪問提供符合用戶閱讀習(xí)慣語言的頁面和數(shù)據(jù)。配置屬性ApplicationContext內(nèi)部有一塊專有區(qū)域用于管理和保存程序運行時所使用的配置屬性。此外ApplicationContext 還有一個子接口 ConfigurableApplicationContext它的設(shè)計與 ConfigurableBeanFactory 之于 BeanFactory 完全一致。ConfigurableApplicationContext 接口中有兩個新增的重要方法分別是 refresh 和 close代表初始化刷新和關(guān)閉 IOC 容器。4.2.2 ApplicationContext 的實現(xiàn)類我們先將 Spring Framework 中 ApplicationContext 的重要實現(xiàn)類都羅列出來如圖所示。根據(jù)加載配置源的不同可以將 ApplicationContext 分為兩個大類基于 XML 配置文件驅(qū)動的實現(xiàn)代表類 ClassPathXmlApplicationContext和基于注解驅(qū)動的實現(xiàn)代表類AnnotationConfigApplicationContext?。無論是哪種實現(xiàn)它們都來自同一個父類 AbstractApplicationContext。AbstractApplicationContext 中定義和實現(xiàn)了絕大部分應(yīng)用上下文的特性和功能包含生命周期實現(xiàn)、容器初始化和銷毀的邏輯、特殊類型 Bean 的處理等。當(dāng)下 Spring Framework 的版本已發(fā)展至 6.0Spring Boot 早已成為企業(yè)項目開發(fā)的必備基礎(chǔ)框架而 Spring Boot 推薦使用注解驅(qū)動配置的方式相應(yīng)地放棄了基于 XML 配置文件的實現(xiàn)方式所以本書主要側(cè)重注解驅(qū)動的 IOC 容器講解。此外ApplicationContext 在 Web 應(yīng)用下還有對應(yīng)的擴展這部分內(nèi)容會在 WebMvc 開發(fā)中講解。4.3 事件驅(qū)動與監(jiān)聽器下面我們學(xué)習(xí)一些 IOC 容器支持的功能特性這些特性在平時的開發(fā)中不一定全部得到使用但想熟練掌握 Spring Framework了解并掌握這些知識點非常重要。4.3.1 觀察者模式本節(jié)講解事件驅(qū)動機制。對于事件與監(jiān)聽器可能讀者會聯(lián)想到觀察者模式。觀察者模式也被稱為發(fā)布訂閱模式也有人叫它 “監(jiān)聽器模式”?它是 GoF 23 設(shè)計模式中行為型模式之一。觀察者模式關(guān)注的點是某一個對象被修改 / 做出某些反應(yīng) / 發(fā)布一個信息等會自動通知依賴它的對象訂閱者?。觀察者模式的三大核心是觀察者、被觀察的主題、訂閱者。觀察者 (Observer) 需要綁定要通知的訂閱者 (Subscriber)并且要觀察指定的主題 (Subject)。4.3.2 Spring 中的觀察者模式Spring Framework 中能夠體現(xiàn)觀察者模式的特性就是事件驅(qū)動和監(jiān)聽器。在觀察者模式中監(jiān)聽器充當(dāng)訂閱者監(jiān)聽特定的事件事件源充當(dāng)被觀察的主題用來發(fā)布事件IOC 容器本身也是事件廣播器可以理解為觀察者。Spring Framework 中事件驅(qū)動的核心可以劃分為 4 部分事件源、事件、廣播器和監(jiān)聽器。(1) 事件源發(fā)布事件的對象。(2) 事件事件源發(fā)布的信息/做出的動作。(3) 廣播器廣播事件給監(jiān)聽器的對象 (ApplicationContext)。ApplicationContext 接口中實現(xiàn)了 ApplicationEventPublisher 接口具備事件廣播器發(fā)布事件的能力。ApplicationEventMulticaster 組合了所有的監(jiān)聽器具備事件廣播器廣播事件的能力。(4) 監(jiān)聽器監(jiān)聽事件的對象。4.3.3 事件與監(jiān)聽器實踐下面通過一個最簡單的示例體會 Spring Framework 中事件與監(jiān)聽器的使用。1.編寫監(jiān)聽器Spring Framework 中內(nèi)置的監(jiān)聽器接口是 ApplicationListener它附帶了一個泛型可以指定監(jiān)聽器“感興趣”的具體事件只有泛型指定類型的事件才會被監(jiān)聽響應(yīng)?。如果我們要自定義監(jiān)聽器則只需實現(xiàn) ApplicationListener 接口。為了快速體會事件和監(jiān)聽器的功能本節(jié)先介紹兩個事件ContextRefreshedEvent 和 ContextClosedEvent它們分別代表容器刷新初始化完畢和即將關(guān)閉。下面編寫一個監(jiān)聽器 ContextRefreshedApplicationListener并指定其監(jiān)聽 ContextRefreshedEvent 事件。注意編寫完畢的監(jiān)聽器需要注冊到 IOC 容器才可以生效在本示例中使用模式注解 組件掃描的方式加載監(jiān)聽器。packagecom.yangjunbo.bean.event.examplea;importorg.springframework.context.ApplicationListener;importorg.springframework.context.event.ContextRefreshedEvent;importorg.springframework.stereotype.Component;ComponentpublicclassContextRefreshedApplicationListenerimplementsApplicationListenerContextRefreshedEvent{OverridepublicvoidonApplicationEvent(ContextRefreshedEventevent){System.out.println(ContextRefreshedApplicationListener監(jiān)聽到ContextRefreshedEvent事件);}}2.編寫啟動類隨后編寫一個測試啟動類 QuickstartListenerApplication使用組件掃描的方式驅(qū)動 IOC 容器并在 IOC 容器初始化完畢后關(guān)閉 IOC 容器。調(diào)用創(chuàng)建 IOC 容器和 close 方法這兩個動作可以在 IOC 容器的內(nèi)部分別產(chǎn)生 ContextRefreshedEvent 和 ContextClosedEvent 事件從而觸發(fā)相應(yīng)的監(jiān)聽器邏輯。運行 main 方法控制臺會根據(jù)流程依次打印如下信息證明監(jiān)聽器已經(jīng)成功執(zhí)行。以上就是 Spring Framework 中事件與監(jiān)聽器的最簡單使用。packagecom.yangjunbo.bean.event.examplea;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassQuickstartListenerApplication{publicstaticvoidmain(String[]args)throwsException{System.out.println(準備初始化IOC容器。。。);AnnotationConfigApplicationContextctxnewAnnotationConfigApplicationContext(com.yangjunbo.bean.event.examplea);System.out.println(IOC容器初始化完成。。。);ctx.close();System.out.println(IOC容器關(guān)閉。。。);}}3.注解式監(jiān)聽器編寫監(jiān)聽器的方式除了通過實現(xiàn) ApplicationListener 接口還可以使用 EventListener 注解注冊監(jiān)聽器。使用注解式監(jiān)聽器時定義監(jiān)聽器的類不再需要實現(xiàn)任何接口而是直接在需要做出事件響應(yīng)的方法上標(biāo)注 EventListener 注解即可。packagecom.yangjunbo.bean.event.examplea;importorg.springframework.context.event.ContextClosedEvent;importorg.springframework.context.event.EventListener;importorg.springframework.stereotype.Component;ComponentpublicclassContextClosedApplicationListener{EventListenerpublicvoidonContextClosedEvent(ContextClosedEventevent){System.out.println(ContextClosedApplicationListener監(jiān)聽到ContextClosedEvent事件);}}之后重新運行 QuickstartListenerApplication 的 main 方法控制臺可以打印出 ContextClosedApplicationListener 監(jiān)聽事件的響應(yīng)證明注解式監(jiān)聽器同樣正確生效。由以上兩種監(jiān)聽器的代碼示例可以總結(jié)出以下幾個結(jié)論ApplicationListener 會在容器初始化階段就準備就緒在容器銷毀時一同銷毀。ApplicationListener 也是 IOC 容器中的普通Bean。IOC容器中有一些內(nèi)置事件供我們監(jiān)聽。4.3.4 Spring 的內(nèi)置事件在 Spring Framework 中已經(jīng)內(nèi)置了一些事件。1.ApplicationEventApplicationEvent 是 Spring Framework 中事件模型的抽象它本是一個抽象類里面沒有定義重要的內(nèi)容只有事件發(fā)生時的時間戳。ApplicationEvent 繼承自 JDK 中原生的觀察者模式的事件模型。ApplicationEvent 表示基礎(chǔ)事件它被設(shè)計為抽象的因為直接發(fā)布基礎(chǔ)事件沒有意義。2.ApplicationContextEventApplicationContextEvent 繼承自 ApplicationEvent由 ApplicationContextEvent 的源碼可知它在構(gòu)造時會將 IOC 容器 ApplicationContext 一并傳入這就意味著事件發(fā)生時可以通過監(jiān)聽器直接取到 ApplicationContext。3.ContextRefreshedEvent 和 ContextClosedEventContextRefreshedEvent 與 ContextClosedEvent 是成對的事件定義分別對應(yīng) IOC 容器刷新完畢但尚未啟動完成以及 IOC 容器已經(jīng)關(guān)閉但尚未銷毀所有 Bean。對于上述兩個事件發(fā)生時機隨著后續(xù)學(xué)習(xí)的推進會對 IOC 容器的生命周期有更全面的認識。4.ContextStartedEvent 和 ContextStoppedEventContextStartedEvent 與 ContextStoppedEvent 事件的觸發(fā)事件跟Bean 的生命周期擴展點Lifecycle 接口相關(guān)。在第 3 章的最后講解了一個 Lifecycle 接口當(dāng)時講了一個要點Lifecycle 的觸發(fā)時機容器調(diào)用 start 和 stop 方法時觸發(fā)。事件名稱觸發(fā)時機關(guān)鍵說明ContextRefreshedEvent容器refresh()完成時所有單例 Bean 已創(chuàng)建、屬性注入、初始化回調(diào)如PostConstruct均已完成。ContextStartedEvent容器start()完成時在ContextRefreshedEvent之后觸發(fā)。此時所有實現(xiàn)了Lifecycle接口的 Bean 的start()方法已被回調(diào)。ContextClosedEvent容器close()執(zhí)行時IOC 容器已關(guān)閉但此時單例 Bean 尚未被銷毀。ContextStoppedEvent容器stop()完成時在ContextClosedEvent之后觸發(fā)。此時所有實現(xiàn)了Lifecycle接口的 Bean 的stop()方法已被回調(diào)但單例 Bean 仍未被銷毀。4.3.5 自定義事件開發(fā)下面考慮一個 Spring Framework 內(nèi)置事件實際的業(yè)務(wù)場景論壇應(yīng)用中當(dāng)新用戶注冊成功后會同時發(fā)送短信、郵件、站內(nèi)信通知用戶注冊成功。在這個場景中如果后續(xù)再添加一個 “發(fā)放論壇積分” 邏輯如何實現(xiàn)才能保證代碼編寫得更優(yōu)雅、更易維護毫無疑問選擇使用合適的設(shè)計模式有助于提高代碼的可維護性。在這個場景中當(dāng)用戶注冊成功后廣播一個 “用戶注冊成功” 事件將用戶信息隨事件廣播出去發(fā)送短信、郵件、站內(nèi)信的監(jiān)聽器監(jiān)聽到注冊成功的事件后會分別執(zhí)行不同形式的通知動作即便是后期再添加 “發(fā)放論壇積分” 邏輯也只需要再注冊一個對應(yīng)的監(jiān)聽器到 IOC 容器不會產(chǎn)生很強的代碼耦合。下面就來演示上述的場景的代碼實現(xiàn)。1.自定義用戶注冊成功事件Spring Framework 中的自定義事件的方式是通過繼承抽象類 ApplicationEvent 實現(xiàn)的定義一個簡單的用戶注冊成功事件RegisterSuccessEvent所有自定義的事件類都需要聲明一個帶有一個參數(shù)的構(gòu)造方法。packagecom.yangjunbo.bean.event.exampleb;importorg.springframework.context.ApplicationEvent;/** * 注冊成功的事件 */publicclassRegisterSuccessEventextendsApplicationEvent{publicRegisterSuccessEvent(Objectsource){super(source);}}2.編寫監(jiān)聽器使用兩種不同的方式編寫三個監(jiān)聽器分別代表短信推送、郵件推送、站內(nèi)信推送。在一個類中可以同時出現(xiàn)多個被 EventListener 注解標(biāo)注的方法即一個類中可以注冊多個注解式事件監(jiān)聽器。packagecom.yangjunbo.bean.event.exampleb;importorg.springframework.context.event.EventListener;importorg.springframework.stereotype.Component;ComponentpublicclassSenderListener{EventListenerpublicvoidsendMessage(RegisterSuccessEventevent){System.out.println(監(jiān)聽到用戶注冊成功發(fā)送站內(nèi)信。。。);}EventListenerpublicvoidsendEmail(RegisterSuccessEventevent){System.out.println(監(jiān)聽到用戶注冊成功發(fā)送郵件中。。。);}}packagecom.yangjunbo.bean.event.exampleb;importorg.springframework.context.ApplicationListener;importorg.springframework.stereotype.Component;ComponentpublicclassSmsSenderListenerimplementsApplicationListenerRegisterSuccessEvent{OverridepublicvoidonApplicationEvent(RegisterSuccessEventevent){System.out.println(監(jiān)聽到用戶注冊成功發(fā)送短信。。。);}}3.編寫注冊邏輯業(yè)務(wù)層只有事件和監(jiān)聽器還不夠還需要有一個事件源作為事件發(fā)布器在應(yīng)用上下文中發(fā)布事件。我們可以編寫一個 RegisterService 類并注入 Spring Framework 內(nèi)置的事件發(fā)布器 ApplicationEventPublisher注入的方式可以選用注解Autowired 自動注入也可以借助 ApplicationEventPublisherAware 實現(xiàn)回調(diào)注入。packagecom.yangjunbo.bean.event.exampleb;importorg.springframework.context.ApplicationEventPublisher;importorg.springframework.context.ApplicationEventPublisherAware;importorg.springframework.stereotype.Service;ServicepublicclassRegisterServiceimplementsApplicationEventPublisherAware{ApplicationEventPublisherpublisher;OverridepublicvoidsetApplicationEventPublisher(ApplicationEventPublisherpublisher){this.publisherpublisher;}publicvoidregister(Stringusername){// 用戶注冊的動作。。。System.out.println(username注冊成功。。。);// 發(fā)布事件publisher.publishEvent(newRegisterSuccessEvent(username));}}4.編寫測試啟動類接下來編寫測試啟動類 RegisterEventApplication。IOC 容器初始化完畢后獲取 RegisterService隨后調(diào)用 register 方法模擬一次用戶注冊的發(fā)生。運行 main 方法控制臺打印出注冊動作以及監(jiān)聽器的觸發(fā)反應(yīng)。packagecom.yangjunbo.bean.event.exampleb;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassRegisterEventApplication{publicstaticvoidmain(String[]args)throwsException{AnnotationConfigApplicationContextctxnewAnnotationConfigApplicationContext(com.yangjunbo.bean.event.exampleb);RegisterServiceregisterServicectx.getBean(RegisterService.class);registerService.register(張三);}}5.調(diào)整監(jiān)聽器的觸發(fā)順序如果業(yè)務(wù)需要調(diào)整為先發(fā)送站內(nèi)信后發(fā)送郵件這就需要我們定制監(jiān)聽器的執(zhí)行順序。Spring Framework 提供了兩種排序的定制方式(1)實現(xiàn) Ordered 接口并重寫 getOrder 方法傳入排序值(2)在需要排序的類/方法上標(biāo)注 Order 注解并傳入排序值默認的排序值為 Ordered.LOWEST_PRECEDENCE即 Integer.MAX_VALUE代表最靠后?。按照這個規(guī)則我們可以在 SenderListener 的 sendMessage 方法上標(biāo)注 Order(0)重新運行啟動類 RegisterEventApplication 的 main 方法觀察控制臺的打印內(nèi)容可以發(fā)現(xiàn)觸發(fā)順序被成功修改。將 sendMessage 方法上 Order 注解的 value 指定為 Integer.MAX_VALUE - 1隨后再次重新運行 RegisterEventApplication 的 main 方法發(fā)現(xiàn)運行結(jié)果發(fā)送站內(nèi)信在發(fā)送郵件中之前證明默認的排序值是 Integer.MAX_VALUE。6.最佳實踐通過使用事件驅(qū)動機制可以很好地解決單體應(yīng)用中業(yè)務(wù)邏輯之間的解耦。實際項目開發(fā)中自定義事件的使用場景恰恰就如本節(jié)演示的這樣針對一個基礎(chǔ)業(yè)務(wù)邏輯牽動多個關(guān)聯(lián)的子業(yè)務(wù)。可以在基礎(chǔ)業(yè)務(wù)邏輯發(fā)生時向 IOC 容器廣播一個特定的事件并將關(guān)聯(lián)的核心業(yè)務(wù)數(shù)據(jù)放入事件中隨后負責(zé)監(jiān)聽該事件的監(jiān)聽器就可得以回調(diào)從而從事件體中獲取業(yè)務(wù)數(shù)據(jù)進而執(zhí)行相應(yīng)的子業(yè)務(wù)邏輯。