關於Spring的bean注入

哒布溜發表於2024-09-10

本篇內容涉及Spring Bean注入型別和方式,以及類載入的一些介紹,還有一些注入問題的解決辦法

1.瞭解Bean

​ 在Spring框架中,Bean是一個被Spring IoC容器管理的物件。Spring IoC(控制反轉)容器負責Bean的例項化、配置和組裝。當你將類標記為Spring管理的Bean時,Spring可以為你建立類的例項,並管理其生命週期。此外,Spring還允許你透過依賴注入(DI)的方式將Bean之間的依賴關係自動注入到Bean中,從而實現了低耦合的設計。
簡單來說,bean是spring容器管理物件的一個名詞概括。專案中涉及到的一些裝配管理,依賴注入,切面與增強,生命週期等都與Bean注入有關。
對於bean,我們要知道Spring框架下大部分物件的建立管理都是交給Spring容器(IOC思想),並進行配置管理,比如:
@Bean,@Component,@Controller,@Service 和@Repository,新增註解後交給Spring容器進行管理,可以透過註解進行依賴注入,在容器啟動的時候是會掃描標註這些註解的類建立 Bean 並放入容器中。

現在常用注入依賴方式:

  • 構造器注入:利用構造方法的引數注入依賴
  • Setter注入:呼叫Setter的方法注入依賴
  • 欄位注入:在欄位上使用 @Autowired、@Resource或 @Inject 註解

2.注入方式

首先是三種注入方式的簡單介紹:

考慮到與spring的匹配度,這裡使用Spring註解 @Autowired而非java標準註解 @Inject

3.1 注入例項

欄位注入

@Controller
public class UserController {

    @Autowired
    private UserService userService;

}

構造器注入

@Controller
public class UserController {

    private final UserService userService;

    public UserController(UserService userService){
        this.userService = userService;
    }

}

Setter注入

@Controller
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService){
        this.userService = userService;
    }
}

3.2 三種方式對比

構造器注入方式可靠性最強,可維護性也最好,配合lombok的註解@RequiredArgsConstructor,不需要手動寫構造方法了。

IDEA不推薦使用@Autowired進行Field注入的原因

在使用IDEA時可能會發現,Field injection is not recommended (欄位注入是不被推薦的)。

原因:

1.使用@Resource則不會提示警告,這是因為@Resource是JSR-250提供的,與@Autowired比對IOC容器的耦合更低(@Autowired是spring提供的),儘管機率很低,但還是要考慮更換容器的可能性,以及更換後程式碼執行是否正常。

2.欄位注入沒法注入不可變物件,而構造器注入可以

3.欄位注入會導致單元測試(mock)也要使用IOC容器

4.構造器注入透過final可以預防執行時bean被修改出現空值,導致空指標,安全性高一些,相對的,靈活性要差一些,比如不如另外兩種注入方式靈活:

  • Setter和欄位注入適用於依賴經常變動的情況
  • 構造器注入,依賴不變

簡單總結如下

從可靠性,可維護性,是否迴圈依賴檢測,效能方面對比:

可靠性主要取決於構造過程中是否可變

可維護性,即可讀性,是否利於維護

對於Bean之間是否存在迴圈依賴關係的檢測能力(srpingboot2.6以後預設配置下只要存在迴圈依賴都無法啟動)

效能主要和啟動順序要求有關

表格對比

注入方式 可靠性 可維護性 迴圈依賴檢測 效能影響
Field Injection 不可靠 不檢測 較快
Constructor Injection 可靠 自動檢測 相對較慢
Setter Injection 不可靠 不檢測 較快

總結來說:推薦使用構造器注入

tips:可以配合lombok註解@RequiredArgsConstructor使用:

@RequiredArgsConstructor
@Service
public class SysOssServiceImpl implements ISysOssService, OssService {

    private final SysOssMapper baseMapper;
}

可省略構造方法書寫

3.特殊業務場景

3.1 注入式Bean為空

工具類或其餘非@Controller,@Service 和@Repository註解修飾的普通類需要呼叫其餘介面,這時直接使用@Autowired或者其他注入註解會無效,debug可以發現對應的bean為空。

我們在專案中,一般在controller層中注入service介面,在service層中注入其它的service介面或者mapper介面都是可以的,但是如果我們要在我們自己封裝的Utils工具類中或者非controller普通類中使用@Resource或@Autowried註解注入Service或者Mapper介面就會出現注入為null的問題

測試背景:有個日誌註解我們用多執行緒非同步去執行,現在給多執行緒非同步報錯統一處理,新增一個handler處理類,然後在日誌儲存方法裡故意丟擲異常,測試handler處理類中mapper是否會空指標。

測試程式碼1:

按照普通業務程式碼那樣去寫:

@Slf4j
@Component
public class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Resource
    private WarningResultMapper warningResultMapper;

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        if (Objects.isNull(warningResultMapper)) {
            System.out.println("bean為null");
        }else {
            System.out.println("bean不為null");
        }
        log.info("測試是否有");
        log.info("Exception message - " + throwable.getMessage());
        log.info("Method name - " + method.getName());
        for (Object param : objects) {
            log.info("Parameter value - " + param);
        }
    }
}

輸出 bean為null

@Resource換成@Autowired也依然是null,我的MyAsyncExceptionHandler裡的方法也不是工具類那種靜態的,mapper為什麼會null呢?有人說是多執行緒防注入。
測試程式碼2:

在多執行緒例項中使用 @Autowired 註解得不到物件,物件為 null,為什麼呢?

這是因為多執行緒是防注入的,所以只是在多執行緒實現類中簡單的使用 @Autowired 方法注入自己的 service,會在程式執行到此類呼叫 service 方法的時候提示注入的 service 為 null。

於是我改為使用 @PostConstruct, 被 @PostConstruct 修飾的方法會在服務載入 Servlet 的時候執行,並且只會被執行一次。PostConstruct 在建構函式之後執行,init () 方法之前執行。PreDestroy()方法在 destroy () 方法執行執行之後執行, 結果能注入成功

使用靜態變數 加 @PostConstruct

@Slf4j
@Component
public class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Resource
    private WarningResultMapper warningResultMapper;
    private static MyAsyncExceptionHandler myAsyncExceptionHandler;
    
    @PostConstruct
    public void init() {
        myAsyncExceptionHandler = this;
        myAsyncExceptionHandler.warningResultMapper = this.warningResultMapper;
    }

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        if (Objects.isNull(myAsyncExceptionHandler.warningResultMapper)) {
            System.out.println("bean為null");
        }else {
            System.out.println("bean不為null");
        }
        log.info("測試是否有");
        log.info("Exception message - " + throwable.getMessage());
        log.info("Method name - " + method.getName());
        for (Object param : objects) {
            log.info("Parameter value - " + param);
        }
    }
}

列印:

bean不為null

注入成功,這種方式也適用於static呼叫的注入報錯(靜態變數是屬於類本身的資訊,當類載入器載入靜態變數時,Spring的上下文環境還沒有被載入,所以不可能為靜態變數繫結值,所以需要@PostConstruct進行初始化。

如果依然有疑惑,請先了解Spring上下文載入與靜態變數的關係,見Tips

Tips

Spring上下文載入

Spring框架的上下文環境(context)負責管理應用中的bean(即物件),包括它們的生命週期和依賴關係。Spring上下文在應用程式啟動時載入,並在此過程中解析配置檔案(如XML檔案或註解),建立並管理bean。

靜態變數

靜態變數是類級別的變數,無論類有多少例項,都會共享靜態變數;靜態變數在類被載入到JVM時進行初始化,一般來說就是第一次 透過new 建立類例項的時候或者訪問靜態方法的時候。

靜態變數與Spring上下文載入的關係

由於靜態變數在類載入時初始化,而Spring上下文是在應用程式啟動時載入的,這意味著在靜態變數初始化時,Spring的上下文可能還沒有被完全載入和初始化。因此,在靜態變數初始化時,你無法直接訪問Spring上下文中的bean或利用Spring的依賴注入功能來為靜態變數賦值。

@PostConstruct註解

@PostConstruct註解用於標記在依賴注入完成後需要執行的方法。這個方法會在建構函式之後、類完全初始化之前被自動呼叫。因此,它提供了一個在Spring上下文載入完成後執行程式碼的機會,這時你可以安全地訪問Spring管理的bean,包括為靜態變數賦值。

類內方法呼叫順序

  • 靜態方法:用static宣告,jvm載入類的時候執行,只執行一次。
  • 建構函式:物件一建立就呼叫相應的建構函式。

(有沒有想起大學試卷裡執行順序的考題了)

測試程式碼3:

還有另一種方法

使用靜態變數,並配合set注入

具體見setMyAsyncExceptionHandler();

這種注入方式也很好理解,就是透過呼叫成員變數的set方法來注入想要使用的依賴物件,但是為什麼mapper物件還要用static修飾呢?不能去掉,然後直接用this賦值嗎,我推測這還是和多執行緒環境有關,用static修飾應該是為了保證:所有該類的例項共享同一個warningResultMapper變數。靜態變數在類被載入到JVM時就被初始化,並且在類的整個生命週期內都存在

@Slf4j
@Component
public class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    // @Resource
    private static WarningResultMapper warningResultMapper;
    private static MyAsyncExceptionHandler myAsyncExceptionHandler;

    /*
        @PostConstruct
        public void init() {
            myAsyncExceptionHandler = this;
            myAsyncExceptionHandler.warningResultMapper = this.warningResultMapper;
        }
    */
    @Autowired
    public void setMyAsyncExceptionHandler(WarningResultMapper warningResultMapper) {
        MyAsyncExceptionHandler.warningResultMapper = warningResultMapper;
    }

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        //if (Objects.isNull(myAsyncExceptionHandler.warningResultMapper)) {
        if (Objects.isNull(warningResultMapper)) {
            System.out.println("bean為null");
        } else {
            System.out.println("bean不為null");
        }
        log.info("測試是否有");
        log.info("Exception message - " + throwable.getMessage());
        log.info("Method name - " + method.getName());
        for (Object param : objects) {
            log.info("Parameter value - " + param);
        }
    }
}

還有一種方法,程式碼注入,這裡偷懶引用別人的demo

@Component //關鍵1
public class ArticlesReceiver {
 
    private static WechatArticlesTempService wechatArticlesTempService =  SpringContextHolder.getBean(WechatArticlesTempService.class); //關鍵2
 
    public WechatArticlesTemp getResposeArticlesBoby(String mediaId) {
      WechatArticlesTemp articlesTemp = wechatArticlesTempService.getById(mediaId); //關鍵3
      return articlesTemp ;
    }
}

3.2 迴圈依賴報錯

專案啟動,迴圈依賴報錯

The dependencies of some of the beans in the application context form a cycle:
XXXX
Requested bean is currently in creation: Is there an unresolvable circular reference?

我們可以在報錯資訊裡找到異常型別:BeanCurrentlyInCreationException,說明這種報錯屬於迴圈依賴問題。

3.2.1 什麼是迴圈依賴

建立新的A時,發現要注入原型欄位B,建立新的B發現要注入原型欄位A,或者A類自己迴圈注入自己,都會報錯。

對於這種情況,Spring提供了處理機制,但處理的方式和徹底性取決於依賴注入的型別

欄位注入,Setter注入

​ spring利用三級快取解決欄位注入迴圈依賴問題,如果sprongboot版本2.6以上,需要先開啟配置 允許迴圈依賴:

spring:
  main:
    allow-circular-references: true

如果不開啟配置啟動時就會檢測報錯,不管你是不是非構造器注入,只要存在迴圈依賴都會啟動報錯

構造器注入

​ 對於透過建構函式注入的bean,Spring無法自動解決迴圈依賴問題。這是因為在構造過程中,bean還未完全初始化,因此無法被注入到其他bean中。如果使用會發現,Spring會丟擲BeanCurrentlyInCreationException異常。

我這次報錯就是因為在構造器注入的前提下試圖自己注入自己:由於spring採用預設單例模式導致報錯迴圈依賴

3.2.2 解決辦法

方案一:利用@Lazy註解

​ @Lazy 的延遲載入打破迴圈依賴:假設現在有AService和BService,透過其它途徑生成 BService 的lazy 的代理物件,不會去走建立BService 的代理物件,然後注入AService 這套流程。
​ 這樣建立AService 的單例物件並放入到單例池中,BService 的bean 在例項化後,注入AService bean 屬性就可以從單例池中載入到AService 的真正的bean ,而不會出現bean 物件不一致的問題。

如下程式碼中的ProjectDocumentService bean注入

// @RequiredArgsConstructor
@Service
public class ProjectDocumentServiceImpl implements ProjectDocumentService {

    @Lazy
    @Autowired
    private  ConDocumentService conDocumentService;
    }

在這個例子中,ProjectDocumentServiceImpl 依賴於 ConDocumentService,但透過使用 @Lazy,Spring 不會在建立 ProjectDocumentServiceImpl 的例項時立即解析 ConDocumentService。相反,它會在第一次訪問 ConDocumentService 欄位時解析它。

方案二:重構程式碼消除迴圈依賴

​ 檢查並重新設計元件之間的關係,使它們不再相互依賴,以解耦為目的,我覺得可以從以下幾個方面考慮:

  • 將部分功能抽離為工具類或靜態方法
  • 改變互動模式,比如藉助設計模式等進行解耦,比如策略模式,我們對策略演算法進行抽離封裝,不同的邏輯處理有不同的策略類,客戶程式碼不再需要直接依賴於具體的策略實現,而是依賴於一個共同的介面(即策略介面),從而實現解耦。

4. 關於spring的bean單例模式

在Spring中,bean可以被定義為兩種模式:prototype(原型)和singleton(單例)

singleton(單例):只有一個共享的例項存在,所有對這個bean的請求都會返回這個唯一的例項。

prototype(原型,多例):對這個bean的每次請求都會建立一個新的bean例項,類似於new。

Spring bean 預設作用域是單例。

5.Java類載入機制

5.1 在Spring啟動後,整個過程涉及到Spring容器載入Bean,也涉及到JVM載入類。二者有什麼聯絡

在Spring框架中,Bean的載入和JVM載入類之間存在密切的聯絡,但它們是兩個不同層次的概念。

JVM載入類

​ JVM載入類是指將類的位元組碼從.class檔案載入到記憶體中,並建立對應的Class物件的過程。這個過程包括類的載入、連結(驗證、準備、解析)和初始化階段。類載入由Java虛擬機器的類載入器(ClassLoader)來完成。
Spring框架本身並不涉及類的載入階段,這是由Java虛擬機器在執行時動態完成的。Spring依賴於JVM載入類的機制來實現依賴注入和管理Bean例項。

Spring載入Bean

​ Spring框架在啟動時會建立一個IOC(控制反轉)容器,也稱為應用上下文。這個容器負責管理Bean的生命週期、依賴注入等任務。
​ 當Spring容器啟動時,它會根據配置檔案(如XML配置、註解或者Java配置類)中的定義,例項化Bean物件並將它們裝配到容器中。這個過程包括Bean的建立、初始化和注入依賴。
​ Spring載入Bean的過程需要依賴JVM載入類的功能。當Spring容器例項化一個Bean時,它首先要求JVM載入該Bean類的位元組碼。然後,Spring根據配置檔案或註解,例項化Bean物件並完成依賴注入。
​ 因此,Spring框架的Bean載入過程依賴於JVM載入類的機制。Spring本身並不負責類的載入,而是在類載入完成後,利用已載入的類來建立和管理Bean例項。這種分工使得Spring框架能夠有效地利用JVM的類載入機制,實現靈活的依賴注入和控制反轉功能。

5.2 Spring在啟動過程中,是否會例項化所有Bean?

​ Spring在啟動過程中確實會例項化所有在配置中定義的Bean,但這並不意味著它會立即呼叫每個Bean的建構函式或者執行它們的初始化方法。實際上,Spring會按需建立Bean的例項,並在需要時進行依賴注入和初始化。

​ 具體來說,當Spring容器啟動時,它會掃描配置檔案(如XML配置、Java配置類或者註解)中的Bean定義。然後,它會根據這些定義例項化Bean,並將它們放入容器的Bean工廠中管理。這個過程稱為Bean的註冊。但是,Spring並不會立即例項化和初始化每個Bean的例項,而是等到某個Bean被需要時才進行例項化和初始化操作。
​ Spring的延遲初始化策略允許應用程式更高效地使用記憶體資源,並且在容器啟動時不必立即建立所有Bean例項。因此,雖然Spring會在啟動時建立所有Bean的定義,但它並不一定會在啟動時就建立所有Bean的實際例項。

5.3 Bean的註冊,Bean按需例項化,延遲初始化

​ 所以這意味著最開始Spring啟動時,所有的Bean例項都會被建立並註冊進入Map,但是例項化和初始化是按需的?準確地說,當Spring容器啟動時,它會建立並註冊所有在配置檔案或者註解中定義的Bean的定義(Bean Definition),而不是所有的Bean例項。這些Bean的定義包括Bean的類資訊、依賴關係等,並被儲存在容器的Bean工廠中,通常是一個Map結構,用於管理這些Bean的後設資料。

具體流程:

Bean的註冊:

Spring會在啟動時掃描配置,解析所有的Bean定義(如@Component、@Service、@Repository等註解或者XML配置中的元素),並將這些定義轉換成內部資料結構(BeanDefinition)。這些BeanDefinition描述了Bean的類、依賴、作用域等資訊。

按需例項化:

當應用程式需要訪問某個Bean時,Spring才會根據對應的BeanDefinition來實際建立該Bean的例項。這時候,Spring會根據Bean的作用域(如單例、原型等)決定是否需要建立新的例項,以及是否需要執行Bean的初始化方法(如@PostConstruct註解標記的方法)。

延遲初始化:

Spring的延遲初始化機制確保只有在需要時才會建立Bean例項,從而節省資源並提高應用程式的啟動效能。即使在容器啟動後,很多Bean可能並不會立即被例項化和初始化,除非有其他Bean或者程式碼依賴它們。

總結來說,Spring在啟動時會註冊所有Bean的定義到Bean工廠中,但實際的Bean例項化和初始化是按需進行的,根據應用程式的需要動態建立和管理。

6. 參考資料

雨凝大佬的文章,簡潔凝練:https://www.rainng.com/field-injection-is-not-recommend/

多執行緒注入null:https://cloud.tencent.com/developer/article/1595799

三種注入方式對比: https://www.cnblogs.com/mili0601/p/15582421.html

類載入機制: https://pdai.tech/md/java/jvm/java-jvm-classload.html

spring Bean載入:https://blog.csdn.net/qq_38096989/article/details/140293882

相關文章