1. 程式人生 > >37、談談Spring Bean的生命週期和作用域?

37、談談Spring Bean的生命週期和作用域?

在企業應用軟體開發中,Java 是毫無爭議的主流語言,開放的 Java EE 規範和強大的開源框架功不可沒,其中 Spring 
毫無疑問已經成為企業軟體開發的事實標準之一。今天這一講,我將補充 Spring 相關的典型面試問題,並談談其部分設計細節。

今天我要問你的問題是,談談 Spring Bean 的生命週期和作用域?

典型回答

Spring Bean 生命週期比較複雜,可以分為建立和銷燬兩個過程。

首先,建立 Bean 會經過一系列的步驟,主要包括:

  •   例項化 Bean 物件。
  •   設定 Bean 屬性。
  •   如果我們通過各種 Aware 介面聲明瞭依賴關係,則會注入 Bean 對容器基礎設施層面的依賴。具體包括 BeanNameAware、BeanFactoryAware 和 ApplicationContextAware,分別會注入 Bean ID、Bean Factory 或者 ApplicationContext。
  •   呼叫 BeanPostProcessor 的前置初始化方法 postProcessBeforeInitialization。
  •   如果實現了 InitializingBean 介面,則會呼叫 afterPropertiesSet 方法。
  •   呼叫 Bean 自身定義的 init 方法。
  •   呼叫 BeanPostProcessor 的後置初始化方法 postProcessAfterInitialization。
  •   建立過程完畢。

 你可以參考下面示意圖理解這個具體過程和先後順序。

第二,Spring Bean 的銷燬過程會依次呼叫 DisposableBean 的 destroy 方法和 Bean 自身定製的 destroy 方法。

Spring Bean 有五個作用域,其中最基礎的有下面兩種:

  •   Singleton,這是 Spring 的預設作用域,也就是為每個 IOC 容器建立唯一的一個 Bean 例項。
  •   Prototype,針對每個 getBean 請求,容器都會單獨建立一個 Bean 例項。

從 Bean 的特點來看,Prototype 適合有狀態的 Bean,而 Singleton 則更適合無狀態的情況。另外,使用 Prototype 
作用域需要經過仔細思考,畢竟頻繁建立和銷燬 Bean 是有明顯開銷的。

如果是 Web 容器,則支援另外三種作用域:

  •   Request,為每個 HTTP 請求建立單獨的 Bean 例項。
  •   Session,很顯然 Bean 例項的作用域是 Session 範圍。
  •   GlobalSession,用於 Portlet 容器,因為每個 Portlet 有單獨的 Session,GlobalSession 提供一個全域性性的 HTTP Session。


考點分析

今天我選取的是一個入門性質的高頻 Spring 面試題目,我認為相比於記憶題目典型回答裡的細節步驟,理解和思考 Bean 生命週期所體現出來的 Spring 設計和機制更有意義。

你能看到,Bean 的生命週期是完全被容器所管理的,從屬性設定到各種依賴關係,都是容器負責注入,並進行各個階段其他事宜的處理,Spring 容器為應用開發者定義了清晰的生命週期溝通介面。

如果從具體 API 設計和使用技巧來看,還記得我在專欄第 13 講提到過的 Marker Interface 嗎,Aware 介面就是個典型應用例子,Bean 可以實現各種不同 Aware 的子介面,為容器以 Callback 形式注入依賴物件提供了統一入口。

言歸正傳,還是回到 Spring 的學習和麵試。關於 Spring,也許一整本書都無法完整涵蓋其內容,專欄裡我會有限地補充:

  •   Spring 的基礎機制。
  •   Spring 框架的涵蓋範圍。
  •   Spring AOP 自身設計的一些細節,前面第 24 講偏重於底層實現原理,這樣還不夠全面,畢竟不管是動態代理還是位元組碼操縱,都還只是基礎,更需要 Spring 層面對切面程式設計的支援。

 

知識擴充套件

首先,我們先來看看 Spring 的基礎機制,至少你需要理解下面兩個基本方面。

  •   控制反轉(Inversion of Control),或者也叫依賴注入(Dependency Injection),廣泛應用於 Spring 框架之中,可以有效地改善了模組之間的緊耦合問題。

從 Bean 建立過程可以看到,它的依賴關係都是由容器負責注入,具體實現方式包括帶引數的建構函式、setter 方法或者AutoWired方式實現。

  •   AOP,我們已經在前面接觸過這種切面程式設計機制,Spring 框架中的事務、安全、日誌等功能都依賴於 AOP 技術,下面我會進一步介紹。

 

第二,Spring 到底是指什麼?

我前面談到的 Spring,其實是狹義的Spring Framework,其內部包含了依賴注入、事件機制等核心模組,也包括事務、O/R Mapping 等功能組成的資料訪問模組,以及 Spring MVC 等 Web 框架和其他基礎元件。

廣義上的 Spring 已經成為了一個龐大的生態系統,例如:

  •   Spring Boot,通過整合通用實踐,更加自動、智慧的依賴管理等,Spring Boot 提供了各種典型應用領域的快速開發基礎,所以它是以應用為中心的一個框架集合。
  •   Spring Cloud,可以看作是在 Spring Boot 基礎上發展出的更加高層次的框架,它提供了構建分散式系統的通用模式,包含服務發現和服務註冊、分散式配置管理、負載均衡、分散式診斷等各種子系統,可以簡化微服務系統的構建。
  •   當然,還有針對特定領域的 Spring Security、Spring Data 等。


上面的介紹比較籠統,針對這麼多內容,如果將目標定得太過寬泛,可能就迷失在 Spring 生態之中,我建議還是深入你當前使用的模組,如 Spring MVC。並且,從整體上把握主要前沿框架(如 Spring Cloud)的應用範圍和內部設計,至少要了解主要元件和具體用途,畢竟如何構建微服務等,已經逐漸成為 Java 應用開發面試的熱點之一。

 

第三,我們來探討一下更多有關 Spring AOP 自身設計和實現的細節。

先問一下自己,我們為什麼需要切面程式設計呢?

切面程式設計落實到軟體工程其實是為了更好地模組化,而不僅僅是為了減少重複程式碼。通過 AOP 等機制,我們可以把橫跨多個不同模組的程式碼抽離出來,讓模組本身變得更加內聚,進而業務開發者可以更加專注於業務邏輯本身。從迭代能力上來看,我們可以通過切面的方式進行修改或者新增功能,這種能力不管是在問題診斷還是產品能力擴充套件中,都非常有用。

在之前的分析中,我們已經分析了 AOP Proxy 的實現原理,簡單回顧一下,它底層是基於 JDK 動態代理或者 cglib 位元組碼操縱等技術,執行時動態生成被呼叫型別的子類等,並例項化代理物件,實際的方法呼叫會被代理給相應的代理物件。但是,這並沒有解釋具體在 AOP 設計層面,什麼是切面,如何定義切入點和切面行為呢?

 

Spring AOP 引入了其他幾個關鍵概念:

  •   Aspect,通常叫作方面,它是跨不同 Java 類層面的橫切性邏輯。在實現形式上,既可以是 XML 檔案中配置的普通類,也可以在類程式碼中用“@Aspect”註解去宣告。在執行時,Spring 架會建立類似Advisor來指代它,其內部會包括切入的時機(Pointcut)和切入的動作(Advice)。
  •   Join Point,它是 Aspect 可以切入的特定點,在 Spring 裡面只有方法可以作為 Join Point。
  •   Advice,它定義了切面中能夠採取的動作。如果你去看 Spring 原始碼,就會發現 Advice、Join Point 並沒有定義在 Spring 自己的名稱空間裡,這是因為他們是源自AOP 聯盟,可以看作是 Java 工程師在 AOP 層面溝通的通用規範。


Java 核心類庫中同樣存在類似程式碼,例如 Java 9 中引入的 Flow API 就是 Reactive Stream 規範的最小子集,通過這種方式,可以保證不同產品直接的無縫溝通,促進了良好實踐的推廣。

具體的 Spring Advice 結構請參考下面的示意圖。


其中,BeforeAdvice 和 AfterAdvice 包括它們的子介面是最簡單的實現。而 Interceptor 則是所謂的攔截器,用於攔截住方法(也包括構造器)呼叫事件,進而採取相應動作,所以 Interceptor 是覆蓋住整個方法呼叫過程的 Advice。通常將攔截器型別的 Advice 叫作 Around,在程式碼中可以使用“@Around”來標記,或者在配置中使用“<aop:around>”。

如果從時序上來看,則可以參考下圖,理解具體發生的時機。

  •   Pointcut,它負責具體定義 Aspect 被應用在哪些 Join Point,可以通過指定具體的類名和方法名來實現,或者也可以使用正則表示式來定義條件。

 

你可以參看下面的示意圖,來進一步理解上面這些抽象在邏輯上的意義。

  •   Join Point 僅僅是可利用的機會。
  •   Pointcut 是解決了切面程式設計中的 Where 問題,讓程式可以知道哪些機會點可以應用某個切面動作。
  •   而 Advice 則是明確了切面程式設計中的 What,也就是做什麼;同時通過指定 Before、After 或者 Around,定義了 When,也就是什麼時候做。


在準備面試時,如果在實踐中使用過 AOP 是最好的,否則你可以選擇一個典型的 AOP 例項,理解具體的實現語法細節,因為在面試考察中也許會問到這些技術細節。

如果你有興趣深入內部,最好可以結合 Bean 生命週期,理解 Spring 如何解析 AOP 相關的註解或者配置項,何時何地使用到動態代理等機制。為了避免被龐雜的原始碼弄暈,我建議你可以從比較精簡的測試用例作為一個切入點,如CglibProxyTests。

另外,Spring 框架本身功能點非常多,AOP 並不是它所支援的唯一切面技術,它只能利用動態代理進行執行時編織,而不能進行編譯期的靜態編織或者類載入期編織。例如,在 Java 平臺上,我們可以使用 Java Agent 技術,在類載入過程中對位元組碼進行操縱,比如修改或者替換方法實現等。在 Spring 體系中,如何做到類似功能呢?你可以使用 AspectJ,它具有更加全面的能力,當然使用也更加複雜。

今天我從一個常見的 Spring 面試題開始,淺談了 Spring 的基礎機制,探討了 Spring 生態範圍,並且補充分析了部分 AOP 
的設計細節,希望對你有所幫助。

 

一課一練

關於今天我們討論的題目你做到心中有數了嗎?今天的思考題是,請介紹一下 Spring 宣告式事務的實現機制,可以考慮將具體過程畫圖。