今天來把 IoC 容器的最後一個大功能講完。
Application-Event-Publisher 是 Spring 框架事件驅動與發布機制中的核心組件之一。在經典的 Pub-Sub 模型當中,通常會存在 Publisher(發布者)和 Subscriber(訂閱者)兩個角色,有些架構切得更細,也會把事件(Event)拉出來當作一個單獨的物件形成 3 個角色的架構。
而 Spring 為了把「發布事件」這個動作,跟「如何把事件派發給監聽器」的複雜邏輯解耦,選擇在這樣的三角架構之上,多引入了一個叫做廣播器(Multicaster)的角色,讓 Publisher 只需要專心發布事件,不用管派發細節。因此,對於 Spring 來說,一個完整的事件發布機制,總共需要 4 個不同的組件協力:分別是事件這個物件本身(Event)、事件發布者(Publisher)、事件廣播器(Multicaster)、和事件監聽器(Listener)。
先來談談事件:事件可以分成早期跟現代兩個時間維度來討論。早期的事件指的是 Application-Event,它是 Spring 裡面的一個抽象類別,內部有 2 個重要的成員變數,分別是 source(代表事件的發送源)和 timestamp(代表事件被建立當下的時間戳)。

Application-Event 的運作邏輯跟 Exception 有點類似:透過繼承 Application-Event,我們可以定義自己的 XxxEvent 來代表不同的事件種類,當然也可以在廣播器的位置,利用 Java 的 instanceof 來做天然的訊息分類與路由。在早期的架構下,所有的事件都需要繼承 Application-Event 才可以進行事件的定義與發布。
現代的 Spring 有對這項限制進行優化 —— 當我們發布事件時,如果傳入的參數不是 Application-Event 的子類別,Spring 會自動將這個參數封裝成一個 ApplicationEvent 的子類(叫做 Payload-Application-Event),讓我們可以直接使用乾淨的 POJO 作為事件傳遞,這也是目前開發當中的主流使用方式。

接著來談發布者:不同於事件可能有數十(甚至上百)個,Spring 的發布者在一般情況下都只會有一個,通常也是 IoC 容器本身(畢竟 Application-Context 都繼承 Publisher 介面了)。介面內定義了 2 個方法,是 publishEvent 的 2 種多型,分別對應上一段講過的「參數本身是 Application-Event」和「參數不是 Application-Event」這兩種情況。
publish-Event 的具體實作出現在 IoC 容器的共同抽象上(Abstract-Application-Context),方法實作得非常簡潔,基本上就是先檢查事件是否為 null(不允許 null 作為事件)、判斷是否為廣播器還沒初始化的早期事件:如果不是早期事件,會直接委託廣播器進行發送,但如果是早期事件,則會將事件暫存,待廣播器創建完成之後,再一口氣將這些 early Application Events 發送出去。
廣播器的初始化時機出現在 Day. 8 IoC 容器那一篇裡,refresh 方法的第 8 步。相關的配置邏輯同樣定義在 Abstract-Application-Context 中。跟發布者相似,在通常情況下,廣播器在整個 Spring 裡面也只會有一個。但就像第一段說的,Spring 解耦了「發布事件」跟「如何把事件派發給監聽器」。發布者已經是容器了,廣播氣就不會是。
廣播器預設的實作流程,會先檢查容器內是否已經有一個 "application-Event-Multicaster" 的 Bean 或 BD 。如果容器內存在(就代表系統內有自定義廣播器),Spring 就會跳過預設的創建流程,直接將 Bean 作為系統的廣播器(或把 BD 完整實例化,再把它指定為廣播器)。但如果沒有這個名稱的 Bean 或 BD(大部分都是這樣),Spring 就會走回兜底的 fallback 流程,創建一個 Simple-Application-Event-Multicaster,並指定為唯一的廣播器。

監聽器的準備與生效機制,跨越了 refresh() 的第 10 步 registerL-isteners() 與第 11 步 finish-Bean-Factory-Initialization()。在現代開發中,監聽器大多直接標註在一般的業務 Service 上(也就是一般會使用到的 @EventListener 註解)。但監聽器的建立並不單純隨著 singleton Bean 的生成同步就緒,Spring 底層拆成了準備監聽名單、與創建實體監聽器 2 個步驟,是個初看有點反直覺的建構邏輯。
至於為什麼要這樣做?我們用「不這樣做會怎麼樣」做一個簡單的反例:某些 Bean 會在 @PostConstruct(或初始化階段)中發布自訂事件(如 XxxReadyEvent),通知其他組件自己已經就緒。如果 Spring 不提前在事件廣播器(Application-Event-Multicaster)中準備好監聽名單,當事件在初始化期間發布時,廣播器便無法得知有哪些 Bean 想聽這個事件,進而導致事件被直接忽略,造成訊息遺失。
因此為了解決這個問題,就像上面說的,Spring 採取了 2 階段的處理策略:在第 10 步的 register-Listeners,Spring 會先透過 BD Map 找出所有實作了 Application-Listener 介面的 Bean,並將它們先行註冊到廣播器中,但此時不進行實例化(主要避免在容器在未完全的情況下,實例化 Bean 可能會時引發 Early Initialization 的一些問題)。後續在第 11 步實例化過程中,如果有 Bean 在初始化(或 @PostConstruct)時發布事件,廣播器便能比對事件的型別,視需要即時催生(getBean)對應的監聽器來接收事件,防止漏接。

至於現代以 @EventListener 為主的業務 Bean,則是在第 11 步所有常規單例 Bean 都初始化完畢後,由 Event-Listener-Method-Processor 統一掃描方法並轉換為監聽器註冊進廣播器。@EventListener 不像 Application-Listener 介面的監聽器可以在 Step. 10 統一蒐集,這代表如果我們想監聽 Bean 初始化或 @PostConstruct 時的早期事件,底層仍仰賴傳統實作 Application-Listener 介面的監聽器。
到這邊為止,我們講完了容器基礎設施篇的所有內容:依序先介紹了 Resource-Loader,簡單說明 Spring 是怎麼建立統一的外部檔案讀取機制,從 classpath, file, url, 或 servlet ...等不同地方拿到相關的檔案資源;然後說了 Environment,可以透過 Resource-Loader 載入不同來源的環境資料,且內部實現了 Profile 的環境隔離,讓同一份專案的程式碼能透過 dev, uat, prod...等不同的環境標籤,一鍵切換環境所需的 Bean 與相關設定。
然後我們簡單聊了 i18n 的實現機制:透過將 Java 原生的 Resource-Bundle 與 Locale 封裝成 Message-Source 物件,讓系統能依語系動態解析多國語系文字,同時也可以使用 Message-Format 進行動態的文字訊息組裝;最後,我們在今天介紹了 Event 機制,藉由 4 種不同的組件,實現了非侵入式的事件驅動,使將跨類別的業務流程可以優雅得行進。
加上 Bean-Factory,這 5 個組件就是 IoC 容器提供的主要功能了。從明天開始,我們會把焦點從「容器的基礎核心轉向「進階擴展與橫切邏輯」,探討 Spring 是怎麼實作 AOP(面向切面編程)的底層邏輯,使開發者可以優雅地抽離通用的非業務邏輯;以及如何借助 SPI(服務提供者介面) 與創建週期的擴展點,讓整個框架具備動態的擴充彈性。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。