控制反轉(Inversion of Control,IoC)與 面向切面程式設計(Aspect Oriented Programming,AOP)是 Spring Framework 中最重要的兩個概念,本章會著重介紹前者,內容包括 IoC 容器以及容器中 Bean 的基礎知識。容器為我們預留了不少擴充套件點,讓我們能定製各種行為,本章的最後我會和大家一起了解一些容器提供的抽象機制。透過這些介紹,希望大家可以對 IoC 容器有個大概的認識。
2.1 IoC 容器基礎知識
Spring Framework 為 Java 開發者提供了強大的支援,開發者可以把底層基礎的雜事拋給 Spring Framework,自己則專心於業務邏輯。本節我們會聚焦在 Spring Framework 的核心能力上,著重瞭解 IoC 容器的基礎知識。
2.1.1 什麼是 IoC 容器
在介紹 Spring Framework 的 IoC 容器前,我們有必要先理解什麼是“控制反轉”。 控制反轉 是一種決定容器如何裝配元件的 模式。只要遵循這種模式,按照一定的規則,容器就能將元件組裝起來。這裏所謂的 容器,就是用來建立元件並對它們進行管理的地方。它牽扯到元件該如何定義、元件該何時建立、又該何時銷燬、它們互相之間是什麼關係等——這些本該在元件內部管理的東西,被從元件中剝離了出來。
需要着重指出一點,元件之間的依賴關係原先是由元件自己定義的,並在其內部維護,而現在這些依賴被定義在容器中,由容器來統一管理,並據此將其依賴的內容注入元件中。在好萊塢,演藝公司具有極大的控制權,藝人將簡歷投遞給演藝公司後就只能等待,被動接受演藝公司的安排。這就是知名的好萊塢原則,它可以總結為這樣的一句話“不要給我們打電話,我們會打給你的”(Don't call us, we'll call you)。IoC 容器背後的思想正是好萊塢原則,即所有的元件都要被動接受容器的控制。
Martin Fowler那篇著名的“Inversion of Control Containers and the Dependency Injection pattern” 中提到“控制反轉”不能很好地描述這個模式,“依賴注入”(Dependency Injection)能更好地描述它的特點。正因如此,我們經常會看到這兩個詞一同出現。
Spring Framework、Google Guice、PicoContainer 都提供了這樣的容器,後文中我們也會把 Spring Framework 的 IoC 容器稱為 Spring 容器。
圖 2-1 是 Spring Framework 的官方文件中的一幅圖,它非常直觀地表達了 Spring IoC 容器的作用,即將業務物件(也就是元件,在 Spring 中這些元件被稱為 Bean,2.2 節會詳細介紹 Bean 的內容)和關於元件的配置後設資料(比如依賴關係)輸入 Spring 容器中,容器就能為我們組裝出一個可用的系統。
圖 2-1 Spring IoC 容器
Spring Framework 的模組按功能進行了拆分,spring-core 和 spring-beans 模組提供了最基礎的功能,其中就包含了 IoC 容器。 BeanFactory
是容器的基礎介面,我們平時使用的各種容器都是它的實現,後文中會看到這些實現的具體用法與區別。
2.1.2 容器的初始化
從圖 2-1 中可以看到,Spring 容器需要配置後設資料和業務物件,因此在初始化容器時,我們需要提供這些資訊。早期的配置後設資料只能以 XML 配置檔案的形式提供,從 2.5 版本開始,官方逐步提供了 XML 檔案以外的配置方式,比如基於註解的配置和基於 Java 類的配置,本書中的大部分示例將採用後兩種方式進行配置。
容器初始化的大致步驟如下(在本章後續的幾節中,我們會分別介紹其中涉及的內容)。
(1) 從 XML 檔案、Java 類或其他地方載入配置後設資料。
(2) 透過 BeanFactoryPostProcessor
對配置後設資料進行一輪處理。
(3) 初始化 Bean 例項,並根據給定的依賴關係組裝物件。
(4) 透過 BeanPostProcessor
對 Bean 進行處理,期間還會觸發 Bean 被構造後的回撥方法。
比如,我們有一個如程式碼示例 2-1 所示的業務物件,它會返回一個字串,可以看到它就是一個最普通的 Java 類。
程式碼示例 2-1 最基本的 Hello
類
package learning.spring.helloworld; public class Hello { public String hello() { return "Hello World!"; } }
在沒有 IoC 容器時,我們需要像程式碼示例 2-2 那樣自己管理 Hello
例項的生命週期,通常是在程式碼中用 new
關鍵字新建一個例項,然後把它傳給具體要呼叫它的物件,下面的程式碼只是個示意,所以就使用 new
關鍵字建立例項後直接呼叫方法了。
程式碼示例 2-2 手動建立並呼叫 Hello.hello()
方法
public class Application { public static void main(String[] args) { Hello hello = new Hello(); System.out.println(hello.hello()); } }
如果是把例項交給 Spring 容器託管,則可以將它配置到一個 XML 檔案中,讓容器來管理它的相關生命週期。可以看到程式碼示例 2-3 只是一個普通的 XML 檔案,透過 <beans/>
這個 Schema 來配置 Spring 的 Bean(Bean 的配置會在 2.2.2 節詳細展開)。爲了使用 Spring 的容器,需要在 pom.xml 檔案中引入 org.springframework:spring-beans
依賴。
程式碼示例 2-3 配置 hello Bean 的 beans.xml 檔案
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="hello" class="learning.spring.helloworld.Hello" /> </beans>
最後像程式碼示例 2-4 那樣,將配置檔案載入容器。 BeanFactory
只是一個最基礎的介面,我們需要選擇一個合適的實現類——在實際工作中,更多情況下會用到 ApplicationContext
的各種實現。此處,我們使用 DefaultListableBeanFactory
這個實現類,它並不關心配置的方式, XmlBeanDefinitionReader
能讀取 XML 檔案中的後設資料,我們透過它載入 CLASSPATH 中的 beans.xml 檔案,將其儲存到 DefaultListableBeanFactory
中,隨後就可以透過 BeanFactory
的 getBean()
方法取得對應的 Bean 了。
getBean()
方法有很多不同的引數列表,例子裡就有兩種,一種是取出 Object
型別的 Bean,然後自己做型別轉換;另一種則是在引數裡指明返回 Bean 的型別,如果實際型別不同的話則會丟擲 BeansException
。
程式碼示例 2-4 載入配置檔案並執行的 Application
類程式碼片段
public class Application { private BeanFactory beanFactory; public static void main(String[] args) { Application application = new Application(); application.sayHello(); } public Application() { beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader((DefaultListableBeanFactory) beanFactory); reader.loadBeanDefinitions("beans.xml"); } public void sayHello() { // Hello hello = (Hello) beanFactory.getBean("hello"); Hello hello = beanFactory.getBean("hello", Hello.class); System.out.println(hello.hello()); } }
在這個例子的 sayHello()
方法中,我們完全不用關心 Hello
這個類的例項是如何建立的,只需獲取例項物件然後使用即可。雖然看起來比程式碼示例 2-3 的行數要多,但當工程複雜度增加之後,IoC 託管 Bean 生命週期的優勢就體現出來了。
2.1.3 BeanFactory
與 ApplicationContext
spring-context 模組在 spring-core 和 spring-beans 的基礎上提供了更豐富的功能,例如事件傳播、資源載入、國際化支援等。前面說過, BeanFactory
是容器的基礎介面, ApplicationContext
介面繼承了 BeanFactory
,在它的基礎上增加了更多企業級應用所需要的特性,透過這個介面,我們可以最大化地發揮 Spring 上下文的能力。表 2-1 列舉了常見的一些 ApplicationContext
實現。
表 2-1 常見的 ApplicationContext
實現
類名 | 說明 |
---|---|
ClassPathXmlApplicationContext | 從 CLASSPATH 中載入 XML 檔案來配置ApplicationContext |
FileSystemXmlApplicationContext | 從檔案系統中載入 XML檔案來配置ApplicationContext |
AnnotationConfigApplicationContext | 根據註解和 Java 類配置ApplicationContext |
相比 BeanFactory
,使用 ApplicationContext
也會更加方便一些,因為我們無須自己去註冊很多內容,例如 AnnotationConfigApplicationContext
把常用的一些後置處理器都直接註冊好了,為我們省去了不少麻煩。所以,在絕大多數情況下,建議大家使用 ApplicationContext
的實現類。
如果要將程式碼示例 2-4
中的 Application.java 從使用 BeanFactory 修改爲使用 ApplicationContext,只需做兩處改動:
(1) 在 pom.xml 檔案中,把引入的 org.springframework:spring-beans
修改爲 org.springframework: spring-context
;
(2) 在 Application.java
中,使用 ClassPathXmlApplicationContext
代替 DefaultListableBeanFactory
和 XmlBeanDefinitionReader
的組合,具體見程式碼示例 2-5。
程式碼示例 2-5 調整後的 Application
類程式碼片段
public class Application { private ApplicationContext applicationContext; public static void main(String[] args) { Application application = new Application(); application.sayHello(); } public Application() { applicationContext = new ClassPathXmlApplicationContext("beans.xml"); } public void sayHello() { Hello hello = applicationContext.getBean("hello", Hello.class); System.out.println(hello.hello()); } }
2.1.4 容器的繼承關係
Java 類之間有繼承的關係,子類能夠繼承父類的屬性和方法。同樣地,Spring 的容器之間也存在類似的繼承關係,子容器可以繼承父容器中配置的元件。在使用 Spring MVC 時就會涉及容器的繼承。
先來看一個例子,如程式碼示例 2-6 所示(修改自上一節的 HelloWorld), Hello
類在輸出的字串中加入一段注入的資訊。
程式碼示例 2-6 可以輸出特定資訊的 Hello
類
public class Application { private ApplicationContext applicationContext; public static void main(String[] args) { Application application = new Application(); application.sayHello(); } public Application() { applicationContext = new ClassPathXmlApplicationContext("beans.xml"); } public void sayHello() { Hello hello = applicationContext.getBean("hello", Hello.class); System.out.println(hello.hello()); } }
隨後,我們也要調整一下 XML 配置檔案,父容器與子容器分別用不同的配置,ID 既有相同的,也有不同的,具體如程式碼示例 2-7 與程式碼示例 2-8 所示。
程式碼示例 2-7 父容器配置 parent-beans.xml 檔案
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="parentHello" class="learning.spring.helloworld.Hello"> <property name="name" value="PARENT" /> </bean> <bean id="hello" class="learning.spring.helloworld.Hello"> <property name="name" value="PARENT" /> </bean> </beans>
程式碼示例 2-8 子容器配置 child-beans.xml 檔案
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="childHello" class="learning.spring.helloworld.Hello"> <property name="name" value="CHILD" /> </bean> <bean id="hello" class="learning.spring.helloworld.Hello"> <property name="name" value="CHILD" /> </bean> </beans>
在 Application
類中,我們嘗試從不同的容器中獲取不同的 Bean(關於 Bean 的內容,我們會在 2.2 節展開),以測試繼承容器中 Bean 的可見性和覆蓋情況,具體如程式碼示例 2-9 所示。
程式碼示例 2-9 修改後的 Application
類程式碼片段
public class Application { private ClassPathXmlApplicationContext parentContext; private ClassPathXmlApplicationContext childContext; public static void main(String[] args) { new Application().runTests(); } public Application() { parentContext = new ClassPathXmlApplicationContext("parent-beans.xml"); childContext = new ClassPathXmlApplicationContext( new String[] {"child-beans.xml"}, true, parentContext); parentContext.setId("ParentContext"); childContext.setId("ChildContext"); } public void runTests() { testVisibility(parentContext, "parentHello"); testVisibility(childContext, "parentHello"); testVisibility(parentContext, "childHello"); testVisibility(childContext, "childHello"); testOverridden(parentContext, "hello"); testOverridden(childContext, "hello"); } private void testVisibility(ApplicationContext context, String beanName) { System.out.println(context.getId() + " can see " + beanName + ": " + context.containsBean(beanName)); } private void testOverridden(ApplicationContext context, String beanName) { System.out.println("sayHello from " + context.getId() +": " + context.getBean(beanName, Hello.class).hello()); } }
這段程式的執行結果如下:
ParentContext can see parentHello: true ChildContext can see parentHello: true ParentContext can see childHello: false ChildContext can see childHello: true sayHello from ParentContext: Hello World! by PARENT sayHello from ChildContext: Hello World! by CHILD
透過這個示例,我們可以得出如下關於容器繼承的通用結論——它們和 Java 類的繼承非常相似,二者的對比如表 2-2 所示。
表 2-2 容器繼承 vs . Java 類繼承
容器繼承 | Java 類繼承 |
---|---|
子上下文可以看到父上下文中定義的 Bean,反之則不行 | 子類可以看到父類的 protected 和 public 屬性和方法,父類看不到子類的 |
子上下文中可以定義與父上下文同 ID 的 Bean,各自都能獲取自己定義的 Bean | 子類可以覆蓋父類定義的屬性和方法 |
關於同 ID 覆蓋 Bean,有時也會引發一些意料之外的問題。如果希望關閉這個特性,也可以考慮禁止覆蓋,透過容器的 setAllowBeanDefinitionOverriding()
方法可以控制這一行為。
2.2 Bean 基礎知識
Bean 是 Spring 容器中的重要概念,這一節就讓我們來著重瞭解一下 Bean 的概念、如何注入 Bean 的依賴,以及如何在容器中進行 Bean 的配置。
2.2.1 什麼是 Bean
Java 中有個比較重要的概念叫做“JavaBeans”,維基百科中有如下描述:
JavaBeans 是 Java 中一種特殊的類,可以將多個物件封裝到一個物件(Bean)中。特點是可序列化,提供無參構造器,提供
Getter
方法和Setter
方法訪問物件的屬性。名稱中的 Bean 是用於 Java 的可重用軟體元件的慣用叫法。
從中可以看到: Bean 是指 Java 中的 可重用軟體元件,Spring 容器也 遵循 這一慣例,因此將容器中管理的可重用元件稱為 Bean。容器會根據所提供的後設資料來建立並管理這些 Bean,其中也包括它們之間的依賴關係。Spring 容器對 Bean 並沒有太多的要求,無須實現特定介面或依賴特定庫,只要是最普通的 Java 物件即可,這類物件也被稱為 POJO(Plain Old Java Object)。
一個 Bean 的定義中,會包含如下部分:
Bean 的名稱,一般是 Bean 的
id
,也可以為 Bean 指定別名(alias);Bean 的具體類資訊,這是一個全限定類名;
Bean 的作用域,是單例(singleton)還是原型(prototype);
依賴注入相關資訊,構造方法引數、屬性以及自動織入(autowire)方式;
建立銷燬相關資訊,懶載入模式、初始化回撥方法與銷燬回撥方法。
我們可以自行設定 Bean 的名字,也可以讓 Spring 容器幫我們設定名稱。Spring 容器的命名方式為類名的首字母小寫,搭配駝峰(camel-cased)規則。比如型別為 HelloService
的 Bean,自動生成的名稱就為 helloService
。
2.2.2 Bean 的依賴關係
所謂“依賴注入”,很重要的一塊就是管理依賴。在 Spring 容器中,“管理依賴”主要就是管理 Bean 之間的依賴。有兩種基本的注入方式——基於構造方法的注入和基於 Setter
方法的注入。
所謂基於構造方法的注入,就是透過構造方法來注入依賴。仍舊以 HelloWorld 為例,如程式碼示例 2-10 所示。
程式碼示例 2-10 透過構造方法傳入字串的 Hello
類
package learning.spring.helloworld; public class Hello { private String name; public Hello(String name) { this.name = name; } public String hello() { return "Hello World! by " + name; } }
對應的 XML 配置檔案需要使用 <constructor-arg/>
傳入構造方法所需的內容,如程式碼示例 2-11 所示。
程式碼示例 2-11 透過構造方法配置 Bean 的 XML 檔案
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="hello" class="learning.spring.helloworld.Hello"> <constructor-arg value="Spring"/> </bean> </beans>
<constructor-arg>
中有不少屬性可以配置,具體如表 2-3 所示。
表 2-3 <constructor-arg/>
的可配置屬性
屬性 | 作用 |
---|---|
value | 要傳給構造方法引數的值 |
ref | 要傳給構造方法引數的 Bean ID |
type | 構造方法引數對應的型別 |
index | 構造方法引數對應的位置,從 0 開始計算 |
name | 構造方法引數對應的名稱 |
基於 Setter
方法的注入,顧名思義,就是透過 Bean 的 Setter
方法來注入依賴。我們在第 2.1.4 節已經看到了對應的例子,具體可以參考程式碼示例 2-6 與程式碼示例 2-7。 <property/>
中的 value
屬性是直接注入的值,用 ref
屬性則可注入其他 Bean。也可以像程式碼示例 2-12 這樣來為屬性注入依賴。
程式碼示例 2-12 <property/>
的用法演示
<bean id="..." class="..."> <property name="xxx"> <!-- 直接定義一個內部的Bean --> <bean class="..."/> </property> <property name="yyy"> <!-- 定義依賴的Bean --> <ref bean="..."/> </property> <property name="zzz"> <!-- 定義一個列表 --> <list> <value>aaa</value> <value>bbb</value> </list> </property> </bean>
手動配置依賴在 Bean 少時還能接受,當 Bean 的數量變多後,這種配置就會變得非常繁瑣。在合適的場合,可以讓 Spring 容器替我們自動進行依賴注入,這種機制稱為 自動織入。自動織入有幾種模式,具體見表 2-4。
表 2-4 自動織入的模式
名稱 | 說明 |
---|---|
no |
不進行自動織入 |
byName |
根據屬性名查詢對應的 Bean 進行自動織入 |
byType |
根據屬性型別查詢對應的 Bean 進行自動織入 |
constructor |
同 byType ,但用於構造方法注入 |
在 <bean/>
中可以透過 autowire
屬性來設定使用何種自動織入方式,也可以在 <beans/>
中設定 default-autowire
屬性指定預設的自動織入方式。在使用自動織入時,需要注意以下事項:
開啟自動織入後,仍可以手動設定依賴,手動設定的依賴優先順序高於自動織入;
自動織入無法注入基本型別和字串;
對於集合型別的屬性,自動織入會把上下文裡找到的 Bean 都放進去,但如果屬性不是集合型別,有多個候選 Bean 就會有問題。
爲了避免第三點中說到的問題,可以將 <bean/>
的 autowire-candidate
屬性設定為 false
,也可以在你所期望的候選 Bean 的 <bean/>
中將 primary
設定為 true
,這就表明在多個候選 Bean 中該 Bean 是主要的(如果使用基於 Java 類的配置方式,我們可以透過選擇 @Primary
註解實現一樣的功能)。
最後,再簡單提一下如何指定 Bean 的初始化順序。一般情況下,Spring 容器會根據依賴情況自動調整 Bean 的初始化順序。不過,有時 Bean 之間的依賴並不明顯,容器可能無法按照我們的預期進行初始化,這時我們可以自己來指定 Bean 的依賴順序。 <bean/>
的 depends-on
屬性可以指定當前 Bean 還要依賴哪些 Bean(如果使用基於 Java 類的配置方式, @DependsOn
註解也能實現一樣的功能)。
2.2.3 Bean 的三種配置方式
Spring Framework 提供了多種不同風格的配置方式,早期僅支援 XML 配置檔案的方式,Spring Framework 2.0 引入了基於註解的配置方式,到了 3.0 則又增加了基於 Java 類的配置方式。這幾種方式沒有明確的優劣之分,選擇合適的或者喜歡的方式就好,很多時候我們也會混合使用這幾種配置方式。
鑑於 Spring 容器的後設資料配置本質上就是配置 Bean(AOP 和事務的配置背後也是配置各種 Bean),因此我們會在本節中詳細展開說明如何配置 Bean。
基於 XML 檔案的配置
Spring Framework 提供了
<beans/>
這個 Schema來配置 Bean,前文中已經用過了 XML 檔案方式的配置,這裏再簡單回顧一下。我們透過
<bean/>
可以配置一個 Bean,id
指定 Bean 的標識,class
指定 Bean 的全限定類名,一般會透過類的構造方法來建立 Bean,但也可以使用一個靜態的factory-method
,比如下面就使用了create()
靜態方法:
<bean id="xxx" class="learning.spring.Yyy" factory-method="create" />
<constructor-arg/>
和 <property/>
用來注入所需的內容。如果是另一個 Bean 的依賴,一般會用 ref
屬性,2.2.3 節中已經有過說明,此處就不再贅述了。
<bean/>
中還有幾個重要的屬性, scope
表明當前 Bean 是單例還是原型, lazy-init
是指當前 Bean 是否是懶載入的, depends-on
明確指定當前 Bean 的初始化順序,就像下面這樣:
<bean id="..." class="..." scope="singleton" lazy-init="true" depends-on="xxx"/>
基於註解的配置
Spring Framework 2.0 引入了
@Required
註解,Spring Framework 2.5 又引入了@Autowired
、@Component
、@Service
和@Repository
等重要的註解,使用這些註解能簡化 Bean 的配置。我們可以像程式碼示例 2-13 那樣開啟對這些註解的支援。
程式碼示例 2-13 啟用基於註解的配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="learning.spring"/> </beans>
上述配置會掃描 learning.spring
包內的類,在類上新增如下四個註解都能讓 Spring 容器把它們配置為 Bean,如表 2-5 所示。
註釋 | 說明 |
---|---|
@Component |
將類標識為普通的元件,即一個 Bean |
@Service |
將類標識為服務層的服務 |
@Repository |
將類標識為資料層的資料倉儲,一般是 DAO(Data Access Object) |
@Controller |
將類標識為 Web 層的 Web 控制器(後來針對 REST 服務又增加了一個 @RestController 註解) |
如果不指定 Bean 的名稱,Spring 容器會自動生成一個名稱,當然,也可以明確指定名稱,比如:
@Component("helloBean") public class Hello {...}
如果要注入依賴,可以使用如下的註解,如表 2-6 所示。
表 2-6 可注入依賴的註解
註解 | 說明 |
---|---|
@Autowired |
根據型別注入依賴,可用於構造方法、 Setter 方法和成員變數 |
@Resource |
JSR-250 的註解,根據名稱注入依賴 |
@Inject |
JSR-330 的註解,同 @Autowired |
從 Spring Framework 6.0 開始,
@Resource
、@PostConstruct
和@PreDestroy
註解都換了新的包名,建議使用jakarta
.annotation 包裡的註解,但也相容 javax.annotation 包中的註解;@Inject 註解則是建議使用jakarta
.inject 包裡的,但也相容 javax.inject 包中的。
@Autowired
註解比較常用,下面的例子中可以指定是否必須存在依賴項,並指定目標依賴的 Bean ID:
@Autowired(required = false) @Qualifier("helloBean") public void setHello(Hello hello) {...}
除此之外,還可以使用 @Value
註解注入環境變數、Properties 或 YAML 中配置的屬性和 SpEL 表示式的計算結果。JSR-250 中還有 @PostConstruct
和 @PreDestroy
註解,把這兩個註解加在方法上用來表示該方法要在初始化後呼叫或者是在銷燬前呼叫,在聊到 Bean 的生命週期時我們還會看到它們。
基於 Java 類的配置
從 Spring Framework 3.0 開始,我們可以使用 Java 類代替 XML 檔案,使用
@Configuration
、@Bean
和@ComponentScan
等一系列註解,基本可以滿足日常所需。透過
AnnotationConfigApplicationContext
可以構建一個支援基於註解和 Java 類的 Spring 上下文:
ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
其中的 Config
類就是一個加了 @Configuration
註解的 Java 類,它可以是程式碼示例 2-14 這樣的。
程式碼示例 2-14 Java 配置類示例
@Configuration @ComponentScan("learning.spring") public class Config { @Bean @Lazy @Scope("prototype") public Hello helloBean() { return new Hello(); } }
類上的 @Configuration
註解表明這是一個 Java 配置類, @ComponentScan
註解指定了類掃描的包名,作用與 <context:component-scan/>
類似。在 @ComponentScan
中, includeFilters
和 excludeFilters
可以用來指定包含和排除的元件,例如官方文件中就有如下示例:
@Configuration @ComponentScan(basePackages = "org.example", includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"), excludeFilters = @Filter(Repository.class)) public class AppConfig { ... }
如果 @Configuration
沒有指定掃描的基礎包路徑或者類,預設就從該配置類的包開始掃描。
Config
類中的 helloBean()
方法上新增了 @Bean
註解,該方法的返回物件會被當做容器中的一個 Bean, @Lazy
註解說明這個 Bean 是延時載入的, @Scope
註解則指定了它是原型 Bean。 @Bean
註解有如下屬性,如表 2-7 所示。
表 2-7 @Bean
註解的屬性
屬性 | 預設值 | 說明 |
---|---|---|
name |
{} |
Bean 的名稱,預設同方法名 |
value |
{} |
同 name |
autowire |
Autowire.NO |
自動織入方式 |
autowireCandidate |
true |
是否是自動織入的候選 Bean |
initMethod |
"" |
初始化方法名 |
destroyMethod |
AbstractBeanDefinition.INFER_METHOD |
銷燬方法名 |
自動推測銷燬 Bean 時該呼叫的方法,會自動去呼叫修飾符為
public
、沒有引數且方法名是close
或shutdown
的方法。如果類實現了java.lang.AutoCloseable
或java.io.Closeable
介面,也會呼叫其中的close()
方法。
在 Java 配置類中指定 Bean 之間的依賴關係有兩種方式,透過方法的引數注入依賴,或者直接呼叫類中帶有 @Bean
註解的方法。
程式碼示例 2-15 中, foo()
建立了一個名為 foo
的 Bean, bar()
方法透過引數 foo
注入了 foo
這個 Bean, baz()
方法內則透過呼叫 foo()
獲得了同一個 Bean。
程式碼示例 2-15 依賴示例
@Configuration public class Config { @Bean public Foo foo() { return new Foo(); } @Bean public Bar bar(Foo foo) { return new Bar(foo); } @Bean public Baz baz() { return new Baz(foo()); } }
需要重點說明的是,Spring Framework 針對 @Configuration
類中帶有 @Bean
註解的方法透過 CGLIB(Code Generation Library)做了特殊處理,針對返回單例型別 Bean 的方法,呼叫多次返回的結果是一樣的,並不會真的執行多次。
在配置類中也可以匯入其他配置,例如,用 @Import 匯入其他配置類,用 @ImportResource 匯入配置檔案,就像下面這樣:
@Configuration @Import({ CongfigA.class, ConfigB.class }) @ImportResource("classpath:/spring/*-applicationContext.xml") public class Config {}
2.3 定製容器與 Bean 的行為
通常,Spring Framework 包攬了大部分工作,替我們管理 Bean 的建立與依賴,將各種元件裝配成一個可執行的應用。然而,有些情況下,我們會有自己的特殊需求。例如,在 Bean 的依賴被注入後,我們想要觸發 Bean 的回撥方法做一些初始化;在 Bean 銷燬前,我們想要執行一些清理工作;我們想要 Bean 感知容器的一些資訊,拿到當前的上下文自行進行判斷或處理……
這時候,怎麼做?Spring Framework 為我們預留了發揮空間。本節我們就來探討一下如何根據自己的需求來定製容器與 Bean 的行為。
2.3.1 Bean 的生命週期
Spring 容器接管了 Bean 的整個生命週期管理,具體如圖 2-2 所示。一個 Bean 先要經過 Java 物件的建立(也就是透過 new
關鍵字建立一個物件),隨後根據容器裡的配置注入所需的依賴,最後呼叫初始化的回撥方法,經過這三個步驟纔算完成了 Bean 的初始化。若不再需要這個 Bean,則要進行銷燬操作,在正式銷燬物件前,會先呼叫容器的銷燬回撥方法。
圖 2-2 Bean 的生命週期
由於一切都是由 Spring 容器管理的,所以我們無法像自己控制這些動作時那樣任意地在 Bean 建立後 或 Bean 銷燬前 增加某些操作。為此,Spring Framework 為我們提供了幾種途徑,在這兩個時間點呼叫我們提供給容器的回撥方法。可以根據不同情況選擇以下三種方式之一:
實現
InitializingBean
和DisposableBean
介面;使用 JSR-250 的
@PostConstruct
和@PreDestroy
註解;在
<bean/>
或@Bean
裡配置初始化和銷燬方法。建立 Bean 後的回撥動作
如果我們希望在建立 Bean 後做一些特別的操作,比如查詢資料庫初始化快取等,Spring Framework 可以提供一個初始化方法。
InitializingBean
介面有一個afterPropertiesSet()
方法——顧名思義,就是在所有依賴都注入後自動呼叫該方法。在方法上新增@PostConstruct
註解也有相同的效果。也可以像下面這樣,在 XML 檔案中進行配置
<bean id="hello" class="learning.spring.helloworld.Hello" init-method="init" />
或者在 Java 配置中指定:
@Bean(initMethod="init") public Hello hello() {...}
銷燬 Bean 前的回撥動作
Spring Framework 既然有建立 Bean 後的回撥動作,自然也有銷燬 Bean 前的觸發操作。
DisposableBean
介面中的destroy()
方法和新增了@PreDestroy
註解的方法都能實現這個目的,如程式碼示例 2-16 所示。程式碼示例 2-16 實現了
DisposableBean
介面的類package learning.spring.helloworld; import org.springframework.beans.factory.DisposableBean; public class Hello implements DisposableBean { public String hello() { return "Hello World!"; } @Override public void destroy() throws Exception { System.out.println("See you next time."); } }
當然,也可以在
<bean/>
中指定destroy-method
,或者在@Bean
中指定destroyMethod
。生命週期動作的組合
如果我們混合使用上文提到的幾種不同的方式,而且這些方式指定的方法還不盡相同,那就需要明確它們的執行順序了。
無論是初始化還是銷燬,Spring 都會按照如下順序依次進行呼叫:
(1) 新增了
@PostConstruct
或@PreDestroy
的方法;(2) 實現了
InitializingBean
的afterPropertiesSet()
方法,或DisposableBean
的destroy()
方法;(3) 在
<bean/>
中配置的init-method
或destroy-method
,@Bean
中配置的initMethod
或destroyMethod
。在程式碼示例 2-17 的 Java 類中,同時提供了三個銷燬的方法。
程式碼示例 2-17 新增了多個銷燬方法的
Hello
類package learning.spring.helloworld; import javax.annotation.PreDestroy; import org.springframework.beans.factory.DisposableBean; public class Hello implements DisposableBean { public String hello() { return "Hello World!"; } @Override public void destroy() throws Exception { System.out.println("destroy()"); } public void close() { System.out.println("close()"); } @PreDestroy public void shutdown() { System.out.println("shutdown()"); } }
在對應的 XML 檔案中配置
destroy-method
,要用<context:annotation-config />
開啟註解支援,如程式碼示例 2-18 所示。程式碼示例 2-18 對應的 beans.xml 檔案
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <bean id="hello" class="learning.spring.helloworld.Hello" destroy-method="close" /> </beans>
程式碼示例 2-19 對執行類簡單做了些調整,增加了關閉容器的動作,以便能讓我們觀察到 Bean 銷燬的動作。
程式碼示例 2-19 啟動用的
Application
類程式碼片段public class Application { private ClassPathXmlApplicationContext applicationContext; public static void main(String[] args) { Application application = new Application(); application.sayHello(); application.close(); } public Application() { applicationContext = new ClassPathXmlApplicationContext("beans.xml"); } public void sayHello() { Hello hello = (Hello) applicationContext.getBean("hello"); System.out.println(hello.hello()); } public void close() { applicationContext.close(); } }
這段程式碼執行後的輸出即代表了三個方法的呼叫順序,是與上述順序一致的:
Hello World! shutdown() destroy() close()
當然,在一般情況下,我們並不會在一個 Bean 上寫幾個作用不同的初始化或銷燬方法。這種情況並不常見,大家瞭解即可。
2.3.2 Aware 介面的應用
在大部分情況下,我們的 Bean 感知不到 Spring 容器的存在,也無須感知。但總有那麼一些場景中我們要用到容器的一些特殊功能,這時就可以使用 Spring Framework 提供的很多 Aware
介面,讓 Bean 能感知到容器的諸多資訊。此外,有些容器相關的 Bean 不能由我們自己來建立,必須由容器建立後注入我們的 Bean 中。
例如,如果希望在 Bean 中獲取容器資訊,可以透過如下兩種方式:
實現
BeanFactoryAware
或ApplicationContextAware
介面;用
@Autowired
註解來注入BeanFactory
或ApplicationContext
。
兩種方式的本質都是一樣的,即讓容器注入一個 BeanFactory
或 ApplicationContext
物件。 ApplicationContextAware
是下面這樣的:
public interface ApplicationContextAware { void setApplicationContext(ApplicationContext applicationContext) throws BeansException; }
在拿到 ApplicationContext
後,就能操作該物件,比如呼叫 getBean()
方法取得想要的 Bean。能直接操作 ApplicationContext
有時可以帶來很多便利,因此這個介面相比其他的 Aware
介面“出鏡率”更高一些。
如果 Bean 希望獲得自己在容器中定義的 Bean 名稱,可以實現 BeanNameAware
介面。這個介面的 setBeanName()
方法就是注入一個代表名稱的字串,也算是一個依賴,因此會在 2.3.1 節提到的初始化方法前被執行。
在 2.3.3 節中會提到 Spring 容器的事件機制,這時就會用到 ApplicationEventPublisher
來發送事件,可以實現 ApplicationEventPublisherAware
介面,從容器中獲得 ApplicationEventPublisher
例項。
Spring Framework 中還有很多其他 Aware
介面,感興趣的話,大家可以查閱官方文件瞭解更多詳情。
2.3.3 事件機制
ApplicationContext
提供了一套事件機制,在容器發生變動時我們可以透過 ApplicationEvent
的子類通知到 ApplicationListener
介面的實現類,做對應的處理。例如, ApplicationContext
在啟動、停止、關閉和重新整理時,分別會發出 ContextStartedEvent
、 ContextStoppedEvent
、 ContextClosedEvent
和 ContextRefreshedEvent
事件,這些事件就讓我們有機會感知當前容器的狀態。
我們也可以自己監聽這些事件,只需實現 ApplicationListener
介面或者在某個 Bean 的方法上增加 @EventListener
註解即可,例如程式碼示例 2-20 和程式碼示例 2-21就用以上兩種方式分別處理了 ContextClosedEvent
事件。
程式碼示例 2-20 用 ApplicationListener
介面處理 ContextClosedEvent
事件的 ContextClosedEventListener
類程式碼片段
@Component @Order(1) public class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> { @Override public void onApplicationEvent(ContextClosedEvent event) { System.out.println("[ApplicationListener]ApplicationContext closed."); } }
程式碼示例 2-21 用 @EventListener
註解處理 ContextClosedEvent
事件的 ContextClosedEventAnnotationListener
類程式碼片段
@Component public class ContextClosedEventAnnotationListener { @EventListener @Order(2) public void onEvent(ContextClosedEvent event) { System.out.println("[@EventListener]ApplicationContext closed."); } }
在執行 ch2/helloworld-event 中的 Application
後會得到如下輸出:
上略…… [ApplicationListener]ApplicationContext closed. [@EventListener]ApplicationContext closed.
可以看到兩個類都處理了 ContextClosedEvent
事件,我們透過 @Order
可以指定處理的順序。
這套機制不僅適用於 Spring Framework 的內建事件,也非常方便我們定義自己的事件,不過該事件必須繼承 ApplicationEvent
,而且產生事件的類需要實現 ApplicationEventPublisherAware
,還要從上下文中獲取到 ApplicationEventPublisher
,用它來發送事件。程式碼示例 2-22 是事件生產者 CustomEventPublisher
類的程式碼片段。
程式碼示例 2-22 生產事件的 CustomEventPublisher
類程式碼片段
@Component public class CustomEventPublisher implements ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; public void fire() { publisher.publishEvent(new CustomEvent("Hello")); } @Override public void setApplicationEventPublisher( ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }
對應的事件監聽程式碼也非常簡單,對應方法如下:
@EventListener public void onEvent(CustomEvent customEvent) { System.out.println("CustomEvent Source: " + customEvent.getSource()); }
在執行 ch2/helloworld-event 中的 Application
後會得到如下輸出:
上略…… CustomEvent Source: Hello 下略……
@EventListener
還有一些其他的用法,比如,在監聽到事件後希望再發出另一個事件,這時可以將方法返回值從 void
修改爲對應事件的型別; @EventListener
也可以與 @Async
註解結合,實現在另一個執行緒中處理事件。關於 @Async
註解,我們會在 2.4.2 節中進行說明。
2.3.4 容器的擴充套件點
Spring 容器是非常靈活的,Spring Framework 中有很多機制是透過容器自身的擴充套件點來實現的,比如 Spring AOP 等。如果我們想在 Spring Framework 上封裝自己的框架或功能,也可以充分利用容器的擴充套件點。
BeanPostProcessor
介面是用來定製 Bean 的,顧名思義,這個介面是 Bean 的後置處理器,在 Spring 容器初始化 Bean 時可以加入我們自己的邏輯。該介面中有兩個方法, postProcessBeforeInitialization()
方法在 Bean 初始化前執行, postProcessAfterInitialization()
方法在 Bean 初始化之後執行。如果有多個 BeanPostProcessor
,可以透過 Ordered
介面或者 @Order
註解來指定執行的順序。程式碼示例 2-23 演示了 BeanPostProcessor
的基本用法。
程式碼示例 2-23 列印資訊的 HelloBeanPostProcessor
類程式碼片段
public class HelloBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if ("hello".equals(beanName)) { System.out.println("Hello postProcessBeforeInitialization"); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if ("hello".equals(beanName)) { System.out.println("Hello postProcessAfterInitialization"); } return bean; } }
我們在對應的 Hello
中,也增加一個帶 @PostConstruct
註解的方法,執行 ch2/helloworld-processor 中的 Application
類,驗證一下方法的執行順序是否與大家預想的一樣:
Hello postProcessBeforeInitialization Hello PostConstruct Hello postProcessAfterInitialization
如果說 BeanPostProcessor
是 Bean 的後置處理器,那 BeanFactoryPostProcessor
就是 BeanFactory
的後置處理器,我們可以透過它來定製 Bean 的配置後設資料,其中的 postProcessBeanFactory()
方法會在 BeanFactory
載入所有 Bean 定義但尚未對其進行初始化時介入。它的用法與 BeanPostProcessor
類似,此處就不再贅述了。需要注意的是,如果用 Java 配置類來註冊,那麼方法需要宣告為 static
。2.4.1 節中會講到的 PropertySourcesPlaceholderConfigurer
就是一個 BeanFactoryPostProcessor
的實現。
需要重點說明一下,由於 Spring AOP 也是透過 BeanPostProcessor
實現的,因此實現該介面的類,以及其中直接引用的 Bean 都會被特殊對待, 不會 被 AOP 增強。此外, BeanPostProcessor
和 BeanFactoryPostProcessor
都僅對當前容器上下文的 Bean 有效,不會去處理其他上下文。
2.3.5 優雅地關閉容器
Java 程序在退出時,我們可以透過 Runtime.getRuntime().addShutdownHook()
方法新增一些鉤子,在關閉程序時執行特定的操作。如果是 Spring 應用,在程序退出時也要能正確地執行一些清理的方法。
ConfigurableApplicationContext
介面擴充套件自 ApplicationContext
,其中提供了一個 registerShutdownHook()
。 AbstractApplicationContext
類實現了該方法,正是呼叫了前面說到的 Runtime.getRuntime().addShutdownHook()
,並且在其內部呼叫了 doClose()
方法。
設想在生產程式碼裡有這麼一種情況:一個 Bean 透過 ApplicationContextAware
注入了 ApplicationContext
,業務程式碼根據邏輯判斷從 ApplicationContext
中取出對應名稱的 Bean,再進行呼叫;問題出現在應用程式關閉時,容器已經開始銷燬 Bean 了,可是這段業務程式碼還在執行,仍在繼續嘗試從容器中獲取 Bean,而且程式碼還沒正確處理此處的異常……這該如何是好?
針對這種情況,我們可以藉助 Spring Framework 提供的 Lifecycle
來感知容器的啟動和停止,容器會將啟動和停止的訊號傳播給實現了該介面的元件和上下文。爲了讓例子能夠簡單一些,我們把問題簡化一下: Hello.hello()
在容器關閉前後返回不同的內容,如程式碼示例 2-2423 所示。
程式碼示例 2-24 實現了 Lifecycle
介面的 Hello
類程式碼片段
public class Hello implements Lifecycle { private boolean flag = false; public String hello() { return flag ? "Hello World!" : "Bye!"; } @Override public void start() { System.out.println("Context Started."); flag = true; } @Override public void stop() { System.out.println("Context Stopped."); flag = false; } @Override public boolean isRunning() { return flag; } }
我們將對應的 Application
類也做相應調整,具體如程式碼示例 2-25 所示。
程式碼示例 2-25 調整後的 Application
類程式碼片段
@Configuration public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class); applicationContext.start(); // 這會觸發Lifecycle的start() Hello hello = applicationContext.getBean("hello", Hello.class); System.out.println(hello.hello()); applicationContext.close(); // 這會觸發Lifecycle的stop() System.out.println(hello.hello()); } @Bean public Hello hello() { return new Hello(); } }
上述程式碼的執行結果如下:
Context Started. Hello World! Context Stopped. Bye!
除此之外,我們還可以藉助 Spring Framework 的事件機制,在上下文關閉時會發出 ContextClosedEvent
,監聽該事件也可以觸發業務程式碼做對應的操作。
茶歇時間:Linux 環境下如何關閉程序
在 Linux 環境下,大家常用
kill
命令來關閉程序,其實是kill
命令給程序傳送了一個訊號(透過kill -l
命令可以檢視訊號列表)。不帶引數的“kill 程序號
”傳送的是SIGTERM(15)
,一般程式在收到這個訊號後都會先釋放資源,再停止;但有時程式可能還是無法退出,這時就可以使用“kill -9 程序號
”,傳送SIGKILL(9)
,直接殺死程序。一般不建議直接使用
-9
,因為非正常地中斷程式可能會造成一些意料之外的情況,比如業務邏輯處理到一半,恢復手段不夠健全的話,可能需要人工介入處理那些執行到一半的內容。
2.4 容器中的幾種抽象
Spring Framework 針對研發和運維過程中的很多常見場景做了抽象處理,比如本節中會講到的針對執行環境的抽象,後續章節中會聊到的事務抽象等。正是因為存在這些抽象層,Spring Framework 才能為我們遮蔽底層的很多細節。
2.4.1 環境抽象
自誕生之日起,Java 程式就一直宣傳自己是“Write once, run anywhere”,但往往現實並非如此——雖然有 JVM 這層隔離,但我們的程式還是需要應對不同的執行環境細節:比如使用了 WebLogic 的某些特性會導致程式很難遷移到 Tomcat 上;此外,程式還要面對開發、測試、預釋出、生產等環境的配置差異;在雲上,不同可用區(availability zone)可能也有細微的差異。Spring Framework 的環境抽象可以簡化大家在處理這些問題時的複雜度,代表程式執行環境的 Environment
介面包含兩個關鍵資訊——Profile 和 Properties,下面我們將詳細展開這兩項內容。
Profile 抽象
假設我們的系統在測試環境中不需要載入監控相關的 Bean,而在生產環境中則需要載入;亦或者針對不同的客戶要求,A 客戶要求我們部署的系統直接配置資料庫連線池,而 B 客戶要求透過 JNDI 獲取連線池。此時,就可以利用 Profile 幫我們解決這些問題。
如果使用 XML 進行配置,可以在
<beans/>
的profile
屬性中進行設定。如果使用 Java 類的配置方式,可以在帶有@Configuration
註解的類上,或者在帶有@Bean
註解的方法上新增@Profile
註解,並在其中指定該配置生效的具體 Profile,就像程式碼示例 2-26 那樣。程式碼示例 2-26 針對開發和測試環境的不同 Java 配置
@Configuration @Profile("dev") public class DevConfig { @Bean public Hello hello() { Hello hello = new Hello(); hello.setName("dev"); return hello; } } @Configuration @Profile("test") public class TestConfig { @Bean public Hello hello() { Hello hello = new Hello(); hello.setName("test"); return hello; } }
透過如下兩種方式可以指定要啟用的 Profile(多個 Profile 用逗號分隔):
ConfigurableEnvironment.setActiveProfiles()
方法指定要啟用的 Profile(透過ApplicationContext.getEnvironment()
方法可獲得Environment
);spring.profiles.active
屬性指定要啟用的 Profile(可以用系統環境變數、JVM 引數等方式指定,能透過PropertySource
找到即可,在第 4 章會詳細介紹 Spring Boot 的屬性載入機制)。
例如,啟動程式時,在命令列中增加 spring.profiles.active
:
▸ java -Dspring.profiles.active="dev" -jar xxx.jar
Spring Framework 還提供了預設的 Profile,一般名為 default
,但也可以透過 ConfigurableEnvironment.setDefaultProfiles()
和 spring.profiles.default
來修改這個名稱。
PropertySource 抽象
Spring Framework 中會頻繁用到屬性值,而這些屬性又來自於很多地方,
PropertySource
抽象就遮蔽了這層差異,例如,可以從 JNDI、JVM 系統屬性(-D
命令列引數,System.getProperties()
方法能取得系統屬性)和作業系統環境變數中載入屬性。在 Spring 中,一般屬性用小寫單詞表示並用點分隔,比如
foo.bar
,如果是從環境變數中獲取屬性,會按照foo.bar
、foo_bar
、FOO.BAR
和FOO_BAR
的順序來查詢。4.3.2 節還有載入屬性相關的內容,屆時還會進一步說明。我們可以像下面這樣來獲得屬性
foo.bar
:public class Hello { @Autowired private Environment environment; public void hello() { System.out.println("foo.bar: " + environment.getProperty("foo.bar")); } }
我們在 2.2.3 節中看到過
@Value
註解,它也能獲取屬性,獲取不到時則返回預設值:public class Hello { @Value("$") // :後是預設值 private String value; public void hello() { System.out.println("foo.bar: " + value); } }
${}
佔位符可以出現在 Java 類配置或 XML 檔案中,Spring 容器會試圖從各種已經配置了的來源中解析屬性。要新增屬性來源,可以在@Configuration
類上增加@PropertySource
註解,例如:@Configuration @PropertySource("classpath:/META-INF/resources/app.properties") public class Config {...}
如果使用 XML 進行配置,可以像下面這樣:
<context:property-placeholder location="classpath:/META-INF/resources/app.properties" />
通常我們的預期是一定能找到需要的屬性,但也有這個屬性可有可無的情況,這時將註解的
ignoreResourceNotFound
或者 XML 檔案的ignore-resource-not-found
設定為true
即可。如果存在多個配置,則可以透過@Order
註解或 XML 檔案的order
屬性來指定順序。也許大家會好奇,Spring Framework 是如何實現佔位符解析的,這一切要歸功於
PropertySourcesPlaceholderConfigurer
這個BeanFactoryPostProcessor
。如果使用<context:property-placeholder/>
,Spring Framework 會自動註冊一個PropertySourcesPlaceholderConfigurer
,如果是 Java 配置,則需要我們自己用@Bean
來註冊一個,例如:@Bean public static PropertySourcesPlaceholderConfigurer configurer() { return new PropertySourcesPlaceholderConfigurer(); }
在它的
postProcessBeanFactory()
方法中,Spring 會嘗試用找到的屬性值來替換上下文中的對應占位符,這樣在 Bean 正式初始化時我們就不會再看到佔位符,而是實際替換後的值。我們也可以定義自己的
PropertySource
實現,將它新增到ConfigurableEnvironment.getPropertySources()
返回的PropertySources
中即可,Spring Cloud Config 其實就使用了這種方式。
2.4.2 任務抽象
看過了與環境相關的抽象後,我們再來看看與任務執行相關的內容。Spring Framework 透過 TaskExecutor
和 TaskScheduler
這兩個介面分別對任務的非同步執行與定時執行進行了抽象,接下來就讓我們一起來了解一下。
非同步執行
Spring Framework 的
TaskExecutor
抽象是在 2.0 版本時引入的,Executor
是 Java 5 對執行緒池概念的抽象,如果瞭解 JUC(java.util.concurrent
)的話,一定會知道java.util.concurrent.Executor
這個介面,而TaskExecutor
就是在它的基礎上又做了一層封裝,讓我們可以方便地在 Spring 容器中配置多執行緒相關的細節。TaskExecutor
有很多實現,例如,同步的SyncTaskExecutor
;每次建立一個新執行緒的SimpleAsyncTaskExecutor
;內部封裝了Executor
,非常靈活的ConcurrentTaskExecutor
;還有我們用的最多的ThreadPoolTaskExecutor
。我們可以像下面這樣直接配置一個
ThreadPoolTaskExecutor
:<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <property name="corePoolSize" value="4"/> <property name="maxPoolSize" value="8"/> <property name="queueCapacity" value="32"/> </bean>
也可使用
<task:executor/>
,下面是一個等價的配置:<task:executor id="taskExecutor" pool-size="4-8" queue-capacity="32"/>
茶歇時間:該怎麼配置執行緒池
如果要在程式中使用執行緒,請不要自行建立
Thread
,而應該儘可能考慮使用執行緒池,並且明確執行緒池的大小—不能無限制地建立執行緒。網上有這樣的建議,對於 CPU 密集型的系統,要儘可能減少執行緒數,建議執行緒池大小配置為“CPU 核數 +1”;對於 IO 密集型系統,爲了避免 CPU 浪費在等待 IO 上,建議執行緒池大小為“CPU 核數 ×2”。當然,這只是一個建議值,具體還是可以根據情況來做調整的。
執行緒池的等待佇列預設為
Integer.MAX_VALUE
,這樣可能會造成任務的大量堆積,所以設定一個合理的等待佇列大小後,就要應對“佇列滿”的情況。“佇列滿”時的處理策略是由RejectedExecutionHandler
決定的,預設是ThreadPoolExecutor.AbortPolicy
,即直接丟擲一個RejectedExecutionException
異常。如果我們能接受直接拋棄任務,也可以將策略設定為ThreadPoolExecutor.DiscardPolicy
或ThreadPoolExecutor.DiscardOldestPolicy
。此外,
ThreadPoolTaskExecutor
還有一個keepAliveSeconds
的屬性,透過它可以調整空閒狀態執行緒的存活時間。如果當前執行緒數大於核心執行緒數,到存活時間後就會清理執行緒。在配置好了
TaskExecutor
後,可以直接呼叫它的execute()
方法,傳入一個Runnable
物件;也可以在方法上使用@Async
註解,這個方法可以是空返回值,也可以返回一個Future
:@Async public void runAsynchronous() {...}
爲了讓該註解生效,需要在配置類上增加
@EnableAsync
註解,或者在 XML 檔案中增加<task:annotation-driven/>
配置,開啟對它的支援。預設情況下,Spring 會為
@Async
尋找合適的執行緒池定義:例如上下文裡唯一的TaskExecutor
;如果存在多個,則用 ID 為taskExecutor
的那個;前面兩個都找不到的話會降級使用SimpleAsyncTaskExecutor
。當然,也可以在@Async
註解中指定一個。請注意 對於非同步執行的方法,由於在觸發時主執行緒就返回了,我們的程式碼在遇到異常時可能根本無法感知,而且丟擲的異常也不會被捕獲,因此最好我們能自己實現一個
AsyncUncaughtExceptionHandler
物件來處理這些異常,最起碼列印一個異常日誌,方便問題排查。定時任務
定時任務,顧名思義,就是在特定的時間執行的任務,既可以是在某個特定的時間點執行一次的任務,也可以是多次重複執行的任務。
TaskScheduler
對兩者都有很好的支援,其中的幾個schedule()
方法是處理單次任務的,而scheduleAtFixedRate()
和scheduleWithFixedDelay()
則是處理多次任務的。scheduleAtFixedRate()
按固定頻率觸發任務執行,scheduleWithFixedDelay()
在第一次任務執行完畢後等待指定的時間後再觸發第二次任務。TaskScheduler.schedule()
可以透過Trigger
來指定觸發的時間,其中最常用的就是接收 Cron 表示式的CronTrigger
了,可以像下面這樣在週一到週五的下午 3 點 15 分觸發任務:scheduler.schedule(task, new CronTrigger("0 15 15 * * 1-5"));
與
TaskExecutor
類似,Spring Framework 也提供了不少TaskScheduler
的實現,其中最常用的也是ThreadPoolTaskScheduler
。上述例子中的scheduler
就可以是一個注入的ThreadPoolTaskScheduler
Bean。我們可以選擇用
<task:scheduler/>
來配置TaskScheduler
:<task:scheduler id="taskScheduler" pool-size="10" />
也可以使用註解,預設情況下,Spring 會在同一上下文中尋找唯一的
TaskScheduler
Bean,有多個的話用 ID 是taskScheduler
的,再不行就用一個單執行緒的TaskScheduler
。在配置任務前,需要先在配置類上新增@EnableScheduling
註解或在 XML 檔案中新增<task:annotation-driven/>
開啟註解支援:@Configuration @EnableScheduling public class Config {...}
隨後,在方法上新增
@Scheduled
註解就能讓方法定時執行,例如:@Scheduled(fixedRate=1000) // 每隔1000ms執行 public void task1() {...} @Scheduled(fixedDelay=1000) // 每次執行完後等待1000ms再執行下一次 public void task2() {...} @Scheduled(initialDelay=5000, fixedRate=1000) // 先等待5000ms開始執行第一次,後續每隔1000ms執行一次 public void task3() {...} @Scheduled(cron="0 15 15 * * 1-5") // 按Cron表示式執行 public void task4() {...}
茶歇時間:本地排程 vs. 分散式排程
上文提到的排程任務都是在一個 JVM 內部執行的,一般我們的系統都是以叢集方式部署的,因此並非所有任務都需要在每臺伺服器上執行,同一時間,叢集中的一臺伺服器能執行就夠了。這時,僅有本節所提供的排程任務支援是不夠的,我們還可以藉助其他排程服務來實現我們的需求,例如,噹噹開源的 ElasticJob。
舉個例子,爲了提升效能,我們會使用多級快取,程式碼優先讀取 JVM 本地快取,沒有命中的話再去讀取 Redis 分散式快取。快取要靠定時任務來重新整理,此時本地排程任務就用來重新整理 JVM 快取,而分散式排程任務就用來重新整理 Redis 快取。當然,我們也可以透過分散式排程來管理每臺機器上的排程任務。
甚至在一些場景中我們還需要對排程任務進行復雜的拆分:一臺機器接收到任務被觸發,接著進行一系列的準備工作,隨後將任務分發到叢集中的其他節點上進行後續處理,以此充分發揮叢集的作用。
總之,排程任務可以是非常複雜的,本節只是簡單地引入這個話題,感興趣的話,大家可以再深入研究。
2.5 小結
透過本章的學習,我們對 Spring Framework 的核心容器及 Bean 的概念已經有了一個大概的瞭解。不僅知道了如何去使用它們,更是深入瞭解瞭如何對一些特性進行定製,如何透過類似事件通知這樣的機制來應對某些問題。
Spring Framework 爲了讓大家專心於業務邏輯,為我們提供了很多抽象來遮蔽底層的實現。本章中的環境抽象和任務抽象就是很好的例子。