Spring 學習筆記(2) Spring Bean

Ethan_Wong 發表於 2021-07-26

一、IoC 容器

IoC 容器是 Spring 的核心,Spring 通過 IoC 容器來管理物件的例項化和初始化(這些物件就是 Spring Bean),以及物件從建立到銷燬的整個生命週期。也就是管理物件和依賴,以及依賴的注入等等。

Spring 提供 2 種不同型別的 IoC 容器:BeanFactory 和 ApplicationContext 容器。

1.1 BeanFactory 容器

BeanFactory 是一個管理 Bean 的工廠,它主要負責初始化各種 Bean, 並呼叫它們的生命週期方法。BeanFactory 是最簡單的 Bean 容器,它由 org.springframework.beans.factory.BeanFactory 介面定義實現。提供了容器最基本的功能。

目前 BeanFactory 沒多少人用,主要是為了能夠相容 Spring 整合的第三方框架,所以目前仍然保留了該介面。下面是官網的解釋

The BeanFactory and related interfaces, such as BeanFactoryAware, InitializingBean, DisposableBean, are still present in Spring for the purposes of backward compatibility with the large number of third-party frameworks that integrate with Spring.

Beanfactory 是 org.springframework.beans 的頂級介面。

1.2 ApplicationContext 容器

ApplicationContext 容器幾乎涵蓋所有 BeanFactory 容器的功能,它繼承了 BeanFactory 介面,由org.springframework.context.ApplicationContext 介面定義實現。在 BeanFactory 的基礎上增加了AOP、事務支援等等功能。現在Spring 實際開發中基本上使用的是 ApplicationContext 容器。

ApplicationContext 是 org.springframework.context 的頂級介面。

ApplicationContext 有兩個常用的實現類,分別是 ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext 。

1.2.1 ClassPathXmlApplicationContext 類

看名字就知道它是從類路徑 ClassPath 中尋找 XML 配置檔案,來完成 ApplicationContext 例項化工作:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("configlocation");
//configlocation 是指定 Spring 配置檔案(XML)的名稱和位置,比如 Beans.xml

1.2.2 FileSystemXmlApplicationContext

該類是從檔案系統中尋找 XML 配置檔案:

ApplicationContext applicationContext = new FileSystemXmlApplicationContext("configlocation");
//configlocation 是從非類路徑外中獲取 XML 的名稱和位置,比如 ”F:/workCode/Beans.xml“

它們都是通過 XML 配置檔案來載入 Bean 的。

二、 Spring Bean 的定義

看官網定義:

In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container.

Bean 是由 Spring IoC 容器管理的物件,容器就能通過反射的形式將容器中準備好的物件注入(這裡使用的是反射給屬性賦值)到需求的元件中去,簡單來說,Spring IoC 容器可以看作是一個工廠,Bean 相當於工廠的產品。 Spring 配置檔案則告訴容器需要哪些 Bean,以及需要哪種方式來裝配 Bean。

Bean 其實就是一個 Java 物件,它是根據 bean 規範編寫出來的類,並且由容器生成的物件就是一個 bean。

Bean 規範:

  1. 所有屬性是 private
  2. 提供預設構造方法
  3. 提供 getter 和 setter
  4. 實現 serializable 介面

它和 POJO 其實是一樣的,只不過是遵循 Bean 規範的 POJO 。

Spring 配置檔案

spring 配置檔案主要支援兩種格式:XML 和 Properties 格式

  • Properties 配置檔案主要以 key-value 鍵值對的形式存在,不能進行其他操作,使用於簡單的屬性配置
  • XML 配置檔案是樹形結構,檔案結構清晰,但是內容比較繁瑣,使用於大型複雜專案

一般來說,Spring 的配置檔案使用 XML 格式。 XML 配置檔案的根元素是,該元素下包含多個子元素。每個都定義了一個 bean ,並描述了該 Bean 如何被裝配到 Spring 容器中。

元素的常用屬性:

屬性名稱 描述
id Bean 的唯一識別符號,Spring 容器對 Bean 的配置和管理都通過該屬性完成。id 的值必須以字母開始,可以使用字母、數字、下劃線等符號。
name name 屬性中可以為 Bean 指定多個名稱,每個名稱之間用逗號或分號隔開。Spring 容器可以通過 name 屬性配置和管理容器中的 Bean。
class 該屬性指定了 Bean 的具體實現類,它必須是一個完整的類名,即類的全限定名。
scope 用於設定 Bean 例項的作用域,屬性值可以為 singleton(單例)、prototype(原型)、request、session 和 global Session。其預設值是 singleton
constructor-arg 元素的子元素,可以使用此元素傳入構造引數進行例項化。該元素的 index 屬性指定構造引數的序號(從 0 開始),type 屬性指定構造引數的型別
property 元素的子元素,用於呼叫 Bean 例項中的 setter 方法來屬性賦值,從而完成依賴注入。該元素的 name 屬性用於指定 Bean 例項中相應的屬性名
ref 等元素的子元索,該元素中的 bean 屬性用於指定對某個 Bean 例項的引用
value 等元素的子元素,用於直接指定一個常量值
list 用於封裝 List 或陣列型別的依賴注入
set 用於封裝 Set 型別的依賴注入
map 用於封裝 Map 型別的依賴注入
entry 元素的子元素,用於設定一個鍵值對。其 key 屬性指定字串型別的鍵值,ref 或 value 子元素指定其值
init-method 容器載入 Bean 時呼叫該方法,類似於 Servlet 中的 init() 方法
destroy-method 容器刪除 Bean 時呼叫該方法,類似於 Servlet 中的 destroy() 方法。該方法只在 scope=singleton 時有效
lazy-init 懶載入,值為 true,容器在首次請求時才會建立 Bean 例項;值為 false,容器在啟動時建立 Bean 例項。該方法只在 scope=singleton 時有效

三、 Spring Bean 的作用域

Spring 容器在初始化一個 Bean 例項時,同時會指定該例項的作用域。Spring 5 支援 6 種作用域。

3.1 singleton

預設的作用域,單例模式。表示在 Spring 容器中只有一個 Bean 例項,Bean 以單例的方式存在。在容器啟動前就建立好了物件,任何時間獲取都是之前建立好的那個物件。配置方式可以預設,因為是預設值。<bean class="..."></bean>

3.2 prototype

原型作用域,多例項模式。每次呼叫 Bean 時都會建立一個新例項。Bean 以多例項的方式存在。容器啟動預設不會建立多例項 bean,每次獲取都會建立一個新的例項 bean 。配置方式為 <bean class="..." scope="prototype"></bean>

3.3 request

在 web 環境下,每次 HTTP 請求都會建立一個 Bean 例項,該作用域只在當前 HTTP Request 內有效。 配置方式為 <bean class="..." scope="request"></bean>

3.4 session

在 web 環境下,每次 HTTP 會話共享一個 Bean 例項,不同的 Session 使用不同的 Bean 例項。該作用域只在當前 HTTP Session 內有效。配置方式為 <bean class="..." scope="session"></bean>

3.5 application

在web 環境下,同一個 web application 共享一個 Bean 例項,該作用域在當前 ServletContext 內有效。

3.6 websocket

在web 環境下,同一個 websocket 共享一個 Bean 例項,該作用域在整個 websocket 中有效。

四、Spring Bean 的註冊方式

Bean 的初始化主要分為兩個過程:Bean 的註冊和 Bean 的例項化。Bean 的註冊主要是指 Spring 通過讀取配置檔案獲取各個 bean 的宣告資訊,並且對這些資訊進行註冊的過程。

4.1 XML 配置檔案註冊方式

在 XML 中配置好後進行註冊

<bean id="person" class="org.springframework.beans.Person">
   <property name="id" value="1"/>
   <property name="name" value="Java"/>
</bean>

4.2 Java 註解註冊方式

可以使用 @Component 或 @Configuration + @Bean 來註冊 Bean

@Component
public class Person {
   private Integer id;
   private String name
   // 忽略其他方法
}
@Configuration               //可以理解為 XML 配置檔案中的 <beans> 標籤
public class Person {
   @Bean                     //可以理解為 XML 配置檔案中的 <bean> 標籤
   public Person  person(){
      return new Person();
   }
   // 忽略其他方法
}

4.3 Java API 註冊方式

使用 BeanDefinitionRegistry.registerBeanDefinition() 方法來註冊 Bean, 程式碼如下:

public class CustomBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor {
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
		RootBeanDefinition personBean = new RootBeanDefinition(Person.class);
		// 新增 Bean
		registry.registerBeanDefinition("person", personBean);
	}
}

五、Spring Bean 的生命週期

Spring 中的 Bean 的生命週期比較複雜,可以表示為: Bean 的定義 -> Bean 的初始化 -> Bean 的使用 -> Bean 的銷燬

Spring 是根據 Bean 的作用域來管理,對於單例項 singleton 作用域的 Bean, Spring 能夠精確地知道 這個 Bean 的完整生命週期;而對於 prototype 作用域的 Bean, Spring 只負責建立, 當容器建立了 Bean 的例項後,Bean 的例項就交給客戶端管理,Spring 容器將不再跟蹤其生命週期。

首先,從上面幾節中看到,關於 Bean 的定義和初始化中的註冊都在配置檔案中或者其他方式提前寫好。下面我們直接從 Bean 初始化中的例項化開始看,一般會有以下幾個過程:

Spring 學習筆記(2) Spring Bean

5.1 例項化 Bean

Spring 啟動, 查詢並載入需要被 Spring 管理的 Bean , 並例項化 Bean ,例項化就是通過容器生成一個 Bean。例項化 Bean 方式主要有三種:類的無參構造方法、靜態工廠、例項工廠。

5.1.1 無參構造方法建立

在配置檔案 XML 中配置 bean, 預設使用了無參構造器建立 bean

<bean id="bean" class="com.spring.demo.Bean"></bean>

然後再通過 getBean() 方法來獲取例項化的 bean

ApplicationContext context = new ClasspathXmlApplicationContext("Bean.xml");
Bean b = (Bean)context.getBean("Bean.xml");

5.1.2 靜態工廠方法建立

同樣也是需要在 XML 中配置 bean :

<bean id="bean" class="com.spring.demo.BeanFactory" factory-method="getBean"></bean>

id 和 class 都定位到某個工廠類,factory-method 表示呼叫到該類 BeanFactory 下的方法來建立物件,而且這個 getBean 方法必須是靜態方法。

同樣是用 getBean() 方法獲取例項化的 bean ,就不贅餘了。

5.1.3 例項工廠方法建立

同樣的,配置 XML 檔案

<bean id="beanfactory" class="com.spring.demo.BeanFactory"></bean>
<bean id="bean" factory-bean="beanfactory" factory-method="getbean"></bean>

例項工廠和靜態工廠的區別在於,該例項化方式工廠方法不需要是靜態的,需要先建立物件(在工廠類中新建一個物件),然後再通過物件呼叫其方法建立 bean。

5.2 設定屬性值(依賴注入)

這個階段需要利用依賴注入完成 Bean 中的所有屬性值的配置注入。容器的注入方法主要有構造方法和 Setter 方法注入。

5.2.1 構造方法注入

注入方式是使用 標籤來實現建構函式的注入,在該標籤中,包含這樣幾種屬性:

  • value: 用於注入基本資料型別以及字串型別的值
  • ref: 注入已經定義好的 Bean
  • type: 用來指定對應的建構函式
  • index: 若建構函式有多個引數的時候,可以使用index 屬性指定引數的位置,給引數的位置進行排序
<bean id="" class="">
    <constructor-arg index="0" value=""></constructor-arg>
    <constructor-arg index="1" ref=""></constructor-arg>
</bean>

5.2.2 Setter 方法注入

Setter 方法注入的方式是目前 Spring 主流的注入方式,它可以利用 Java Bean 規範所定義的 Setter/Getter 方法來完成注入,可讀性和靈活性都很高,它不需要使用宣告式構造方法,而是使用 Setter 注入直接設定相關的值。

<bean id="person" class="org.springframework.beans.Person">
    <property name="id" value="1"/>
    <property name="name" value="Java"/>
</bean>

在 Spring 例項化 Bean 的過程中,首先會呼叫預設的構造方法例項化 Bean 的物件,然後通過 Java 的反射機制呼叫 set 方法進行屬性的注入。因此,setter 注入要求 Bean 的對應類必須滿足一下要求:

  • 必須提供一個預設的無參構造方法
  • 必須為需要注入的屬性提供對應的 setter 方法

5.3 呼叫 Aware 的相關方法

5.3.1 呼叫 BeanNameAware 的 setBeanName() 方法

如果 Bean 實現了 BeanNameAware 介面,則 Spring 呼叫 Bean 的 setBeanName() 方法傳入當前 Bean 的 id 值。

5.3.2 呼叫 BeanFactoryAware 的 setBeanFactory() 方法

如果 Bean 實現了 BeanFactoryAware 介面,則 Spring 呼叫 setBeanFactory() 方法傳入當前工廠例項的引用。

5.3.3 呼叫 ApplicationContextAware 的 setApplicationContext()方法

如果 Bean 實現了 ApplicationContextAware 介面,則 Spring 呼叫 setApplicationContext() 方法傳入當前 ApplicationContext 例項的引用。

5.4 呼叫 BeanPostProcessor 的預初始化方法

如果 Bean 實現了 BeanPostProcessor 介面,則 Spring 呼叫該介面的預初始化方法 postProcessBeforeInitialzation() 對 Bean 進行加工操作,此處非常重要,Spring 的 AOP 就是利用它實現的。

5.5 呼叫InitializingBean 的 afterPropertiesSet() 方法和定製的初始化方法

InitializingBean 是一個介面,它有一個 afterPropertiesSet() 方法,在 Bean 初始化時會判斷當前 Bean 是否實現了 InitializingBean,如果實現了則呼叫 afterPropertiesSet() 方法,進行初始化工作;然後再檢查是否也指定了 init-method,如果指定了則通過反射機制呼叫指定的 init-method 方法,它的實現原始碼如下:

protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
        throws Throwable {
    // 判斷當前 Bean 是否實現了 InitializingBean,如果是的話需要呼叫 afterPropertiesSet()
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        if (logger.isTraceEnabled()) {
            logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
        }
        if (System.getSecurityManager() != null) { // 安全模式
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                    ((InitializingBean) bean).afterPropertiesSet(); // 屬性初始化
                    return null;
                }, getAccessControlContext());
            } catch (PrivilegedActionException pae) {
                throw pae.getException();
            }
        } else {
            ((InitializingBean) bean).afterPropertiesSet(); // 屬性初始化
        }
    }
    // 判斷是否指定了 init-method()
    if (mbd != null && bean.getClass() != NullBean.class) {
        String initMethodName = mbd.getInitMethodName();
        if (StringUtils.hasLength(initMethodName) &&
                !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 利用反射機制執行指定方法
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

5.6 呼叫 BeanPostProcessor 的後初始化方法

如果 BeanPostProcessor 和 Bean 關聯,則 Spring 將呼叫該介面的初始化方法 postProcessAfterInitialization()。此時,Bean 已經可以被應用系統使用了。

5.7 Bean 的使用

如果在 中指定了該 Bean 的作用域為 singleton,則將該 Bean 放入 Spring IoC 的快取池中,觸發 Spring 對該 Bean 的生命週期管理;如果在 中指定了該 Bean 的作用域為 prototype,則將該 Bean 交給呼叫者,呼叫者管理該 Bean 的生命週期,Spring 不再管理該 Bean。

5.8 Bean 的銷燬

在 Spring 容器關閉時會執行銷燬方法,但是 Spring 容器不會自動去呼叫銷燬方法,而是需要我們主動的呼叫。

如果是 BeanFactory 容器,那麼我們需要主動呼叫 destroySingletons() 方法,通知 BeanFactory 容器去執行相應的銷燬方法;如果是 ApplicationContext 容器,那麼我們需要主動呼叫 registerShutdownHook() 方法,告知 ApplicationContext 容器執行相應的銷燬方法。

5.9 小結

一般情況下,會在 Bean 被初始化後和被銷燬前執行一些相關操作。

Spring 官方提供了 3 種方法實現初始化回撥和銷燬回撥:

  1. 實現 InitializingBean 和 DisposableBean 介面;
  2. 在 XML 中配置 init-method 和 destory-method;
  3. 使用 @PostConstruct 和 @PreDestory 註解。
    在一個 Bean 中有多種生命週期回撥方法時,優先順序為:註解 > 介面 > XML。

不建議使用介面和註解,這會讓 pojo 類和 Spring 框架緊耦合。

六、Spring Bean 自動裝配

Bean 的裝配可以理解為依賴關係注入,Bean 的裝配方式也就是 Bean 的依賴注入方式。Spring 容器支援多種裝配 Bean 的方式,如基於 XML 的 Bean 裝配、基於 Annotation 的 Bean 裝配和自動裝配等。基於 XML 的裝配方式主要分為兩種,在 5.2 設定屬性值 中提到的過。

自動裝配就是指 Spring 容器在不使用 標籤的情況下,可以自動裝配(autowire)相互協作的 Bean 之間的關聯關係,將一個 Bean 注入其他 Bean 的 Property 中。使用自動裝配需要配置 元素的 autowire 屬性。autowire 屬性有五個值,具體說明如下表所示。

名稱 說明
no 預設值,表示不使用自動裝配,Bean 依賴必須通過 ref 元素定義。
byName 根據 Property 的 name 自動裝配,如果一個 Bean 的 name 和另一個 Bean 中的 Property 的 name 相同,則自動裝配這個 Bean 到 Property 中。
byType 根據 Property 的資料型別(Type)自動裝配,如果一個 Bean 的資料型別相容另一個 Bean 中 Property 的資料型別,則自動裝配。
constructor 類似於 byType,根據構造方法引數的資料型別,進行 byType 模式的自動裝配。
autodetect(3.0版本不支援) 如果 Bean 中有預設的構造方法,則用 constructor 模式,否則用 byType 模式。

5.10 基於註解裝配 Bean

儘管可以使用 XML 來裝配 Bean , 但是如果應用中 Bean 數量過多,會導致 XML 配置檔案過於臃腫,對後期維護帶來一定的困難

Java 從 JDK 5.0 以後,提供了 Annotation(註解)功能,Spring 2.5 版本開始也提供了對 Annotation 技術的全面支援,我們可以使用註解來配置依賴注入。Spring 預設不使用註解裝配 Bean,因此需要在配置檔案中新增 < context:annotation-config >,啟用註解。

Spring 中常用的註解如下。

5.10.1 @Component

可以使用此註解描述 Spring 中的 Bean,但它是一個泛化的概念,僅僅表示一個元件(Bean),並且可以作用在任何層次。使用時只需將該註解標註在相應類上即可。

5.10.2 @Repository

用於將資料訪問層(DAO層)的類標識為 Spring 中的 Bean,其功能與 @Component 相同。

5.10.3 @Service

通常作用在業務層(Service 層),用於將業務層的類標識為 Spring 中的 Bean,其功能與 @Component 相同。

5.10.4 @Controller

通常作用在控制層(如 Struts2 的 Action、SpringMVC 的 Controller),用於將控制層的類標識為 Spring 中的 Bean,其功能與 @Component 相同。

5.10.5 @Autowired

可以應用到 Bean 的屬性變數、屬性的 setter 方法、非 setter 方法及建構函式等,配合對應的註解處理器完成 Bean 的自動配置工作。預設按照 Bean 的型別進行裝配。

5.10.6 @Resource

作用與 Autowired 相同,區別在於 @Autowired 預設按照 Bean 型別裝配,而 @Resource 預設按照 Bean 例項名稱進行裝配。

@Resource 中有兩個重要屬性:name 和 type。

Spring 將 name 屬性解析為 Bean 的例項名稱,type 屬性解析為 Bean 的例項型別。如果指定 name 屬性,則按例項名稱進行裝配;如果指定 type 屬性,則按 Bean 型別進行裝配。如果都不指定,則先按 Bean 例項名稱裝配,如果不能匹配,則再按照 Bean 型別進行裝配;如果都無法匹配,則丟擲 NoSuchBeanDefinitionException 異常。

5.10.7 @Qualifier

與 @Autowired 註解配合使用,會將預設的按 Bean 型別裝配修改為按 Bean 的例項名稱裝配,Bean 的例項名稱由 @Qualifier 註解的引數指定。

參考文章

  1. The IoC container

  2. spring bean是什麼

  3. POJO、JavaBean、Spring Bean 的解釋和區別

  4. 底層原始碼分析 Spring 的核心功能和執行流程?