Spirng 必知概念

zhzcc發表於2024-09-21

Bean作用域

名稱 作用域
singleton 單例物件,預設值的作用域
prototype 每次獲取都會建立一個新的 Bean 例項
request 每一次 HTTP 請求都會產生一個新的 Bean,該 Bean 僅當前 HTTP request 內有效
session 在一次 HTTP session 中,容器將返回同一個例項
global-session 將物件存入到 web 專案叢集的 session 域中,若不存在叢集,則 global session 相當於 session

預設作用域是 Sigleton,多執行緒訪問同一個 Bean 時會存線上程不安全問題

保障執行緒安全方法

  • 在 Bean物件中儘量避免定義可變的成員變數(不太實際)
  • 在類中定義一個 ThreadLocal 成員變數,將需要的可變成員變數儲存在 ThreadLocal 中

啟動初始化

@PostConstruct

在 Bean 建立期間由 Spring 呼叫的初始化方法

  • 使用:將 @PostConstruct 註解新增到方法上就行

InitializingBean

實現 InitializingBean 介面,讓 Spring 呼叫某一個初始化方法

Controller 和 Service 是否執行緒安全的

預設情況下,Scope 值是單例(Singleton)的,是執行緒不安全的。

儘量不要在 @Controller / @Service 等容器中定義靜態變數,不論是單例還是多例都是執行緒不安全的。

BeanFactoryPostProcessor 和 BeanPostProcessor

BeanFactoryPostProcessor:

  • 在 Bean 工程例項化 Bean 之前對 Bean 的定義進行修改
  • ta可以讀取和修改 Bean的定義後設資料,例如修改 Bean 的屬性值、新增額外的配置資訊等

BeanPostProcessor:

  • 在 Bean 例項化後對 Bean 進行和增強或修改
  • ta可以在 Bean 的初始化過程中對 Bean 進行後處理,例如對 Bean 進行代理、新增額外的功能等

Bean 的生命週期

Spring 的生命週期大致分為:建立 -> 屬性填充 -> 初始化 Bean -> 使用 -> 銷燬 幾個核心階段

  • 建立階段:主要是建立物件,物件的建立權交由 Spring 管理
  • 屬性填充階段:主要是進行依賴的注入,將當前物件以來的 Bean 物件,從 Spring 容器中拿出,然後填充到對應的屬性中
  • 初始化 Bean 階段:包括回撥各種 Aware 介面、回撥各種初始化方法、生成 AOP 代理物件也在該階段進行,該階段主要是完成初始化回撥
  • 使用 Bean 階段:主要是 Bean 建立完成,在程式執行期間,提供服務的階段
  • 銷燬 Bean 階段:主要是容器關閉或停止服務,對 Bean 進行銷燬處理
graph LR A(Bean 例項化) --> B(注入物件屬性) --> C(執行 Aware) --> D(BeanPostProcessor 前置處理器) --> E(InitializingBean\init-method 檢查和執行) --> F(BeanPostProcessor 後置處理) --> G(註冊回撥方法) --> H(Bean 使用) --> I(執行銷燬方法)

迴圈依賴

迴圈依賴指:一個例項或多個例項存在相互依賴的關係(類之間迴圈巢狀引用)

參考如下程式碼:

public class AService {
    private BService bService;
}
public class BService {
    private AService aService;
}

Bean 的建立步驟

在建立 Bean 之前,Spring 會透過掃描獲取 BeanDefinition,

BeanDefinition 就緒後會讀取 BeanDefinition 中所對應的 class 來載入類。

例項化階段:根據建構函式來完成例項化(沒有屬性注入以及初始化的物件(原始物件))

屬性注入階段:對 Bean 的屬性進行依賴注入。

若 Bean 的某個方法有 AOP 操作,則需要根據原始物件生成 代理物件

  • AOP 代理是 BeanPostProcessor 實現的,而 BeanPostProcessor 是發生在屬性注入階段後的。

最後把代理物件放入單例池(一級快取 singletonObjects)中。

為什麼 Spring Bean 會產生迴圈依賴問題?

以參考程式碼分析

當 AService 建立時,會對 AService 例項化生成一個原始物件,

然後在進行屬性注入時發現了需要 BService 對應的 Bean,

此時就會去 BService 進行建立,在 BService 例項化後生成一個原始物件後進行屬性注入,此時會發現也需要 AService 對應的 Bean。

三大迴圈依賴問題場景:

1、單例作用下的 Setter 方法注入/Field 屬性注入出現的迴圈依賴

  • 在 Spring 中 是不會 產生迴圈依賴問題的,這主要是靠 三級快取 機制解決

2、單例作用域下的構造器注入出現的迴圈依賴

  • 構造注入 發生在 例項化階段,而 Spring 解決迴圈依賴問題依靠的 三級快取 是在 屬性注入階段,也就是說呼叫建構函式時還未能放入三級快取中,所以無法解決 構造器注入 的迴圈依賴問題

3、原型作用域下的屬性注入出現的迴圈依賴問題

  • 因 Spring 不會快取 原型 作用域的 Bean,所以 Spring 無法解決 原型 作用域的 Bean

三級快取:

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory> singletonFactories = new ConcurrentHashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
  • 一級快取(singletonObjects):快取的是 已經例項化、屬性注入、初始化後的 Bean物件
  • 二級快取(earlySingletonObjects):快取的是例項化後,但未屬性注入、初始化的 Bean物件(用於提前暴露 Bean)
  • 三級快取(singletonFactories):快取的是一個 ObjectFactory,主要作用是生成原始物件進行 AOP 操作後的代理物件(這一級快取主要用於解決 AOP 問題)

為什麼快取可以解決迴圈依賴問題?

在建立 AService 時,例項化後將 原始物件 存放到快取中(提早暴露),

然後依賴注入時發現需要 BService,便會去建立 BService,例項化後同樣 原始物件 存放到快取中,然後依賴注入時發現需要 AService 便會從快取中取出並注入,這樣 BService 就完成了建立,隨後 AService 也能完成屬性注入,最後也完成建立。

三級快取

為什麼還需要第三級快取?

第三級快取(singletonFactories)是為了 Spring 的 AOP 做處理的。

1、如果 AService 中方法沒有使用 AOP 操作,會發現 BService 注入的原始物件與最後 *AService 完成建立的最終物件是 同一個物件

2、如果 AService 方法中有 AOP 操作時,當 AService 的原始物件賦值(注入)給 BServiceAService會進行 AOP 操作產生一個 代理物件,這個代理物件最終會被放入單例池(一級快取)中,也就是說此時 BService中注入的物件是原始物件,而 AService 最終建立完成後是代理物件,這樣就會導致 Bservice 依賴的 AService 和 最終的 AService 不是同一個物件
出現這個問題的原因:AOP 是透過 BeanPostProcess 實現的,而 BeanPostProcessor 是在屬性注入階段後 才執行的,所以會導致 注入的物件有可能和最終的物件不一致

graph LR A(例項化Bean) --> B(生成原始物件) --> C(完成屬性注入) --> D(AOP) --> E(生成代理物件) --> F(放入單例池)

Spring 是如何透過第三級快取來避免 AOP 問題的?

三級快取透過利用 ObjectFactorygetEarlyBeanReference() 做到了提前執行 AOP 操作從而生成代理物件。

在上移到二級快取時,可以做到如果 Bean 中有 AOP 操作,那麼提前暴露的物件會是 AOP 操作後返回的代理物件;如果沒有 AOP 操作,那麼提前暴露的物件會是原始物件。

  • 只有等完成了屬性注入、初始化後的 Bean 才會上移到一級快取(單例池)中

這樣就能做到出現迴圈依賴問題時,注入依賴的物件和最終生成的物件是同一個物件。

  • 相當於 AOP 提前在屬性注入前完成,這樣就不會導致後面生成的代理物件與屬性注入時的物件不一致

相關文章