點擊進入我的博客
1 Spring容器整體流程
1.1 ApplicationContext內部原理
AbstractApplicationContext是ApplicationContext的抽象實現類,其中最重要的是refresh()方法,它定義了容器在加載配置文件以後的各項處理過程。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// (1)初始化BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// (2)調用工廠後處理器
invokeBeanFactoryPostProcessors(beanFactory);
// (3)註冊Bean後處理器
registerBeanPostProcessors(beanFactory);
// (4)初始化消息源
initMessageSource();
// (5)初始化應用上下文事件廣播器
initApplicationEventMulticaster();
// (6)初始化其他特殊Bean,由具體子類實現
onRefresh();
// (7)註冊事件監聽器
registerListeners();
// (8)初始化所有單實例的Bean(Lazy加載的除外)
finishBeanFactoryInitialization(beanFactory);
// (9)完成刷新併發布容器刷新事件
finishRefresh();
}
catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
- 初始化BeanFactory:根據配置文件實例化BeanFactory,在obtainFreshBeanFactory()方法中,首先調用refreshBeanFactory()刷新BeanFactory,然後調用getBeanFactory()方法獲取BeanFactory,這兩個方法都是需要子類實現的抽象方法。在這一步裏,Spring將配置文件的信息裝入到容器的Bean定義註冊表(BeanDefinitionRegistry)中,但此時Bean還未初始化。
- 調用工廠後處理器:根據反射機制從BeanDefinitionRegistry中找出所有BeanFactoryPostProcessor類型的Bean,並調用其postProcessBeanFactory()接口方法。
- 註冊Bean後處理器:根據反射機制從BeanDefinitionRegistry中找出所有BeanPostProcessor類型的Bean,並將它們註冊到容器Bean後處理器的註冊表中。
- 初始化消息源:初始化容器的國際化信息資源。
- 初始化應用上下文事件廣播器。
- 初始化其他特殊的Bean:這是一個鉤子方法,子類可以藉助這個鉤子方法執行一些特殊的操作——如AbstractRefreshableWebApplicationContext就使用該鉤子方法執行初始化ThemeSource的操作。
- 註冊事件監聽器。
- 初始化singleton的Bean:實例化所有singleton的Bean(使用懶加載的吹),並將它們放入Spring容器的緩存中。
- 發佈上下文刷新事件:創建上下文刷新事件,事件廣播器負責將些事件廣播到每個註冊的事件監聽器中。
1.2 Spring創建Bean流程
下圖描述了Spring容器從加載配置文件到創建一個Bean的完整流程:
- ResourceLoader從存儲介質中加載Spring配置文件,並使用Resource表示這個配置文件的資源。
- BeanDefinitionReader讀取Resource所指向的配置文件資源,然後解析配置文件。配置文件中每一個<bean>解析成一個BeanDefinition對象,並保存到BeanDefinitionRegistry中;
- 容器掃描BeanDefinitionRegistry中的BeanDefinition,使用Java的反射機制自動識別出Bean工廠後處理器(實現BeanFactoryPostProcessor接口)的Bean,然後調用這些Bean工廠後處理器對BeanDefinitionRegistry中的BeanDefinition進行加工處理。主要完成以下兩項工作:
3.1 對使用到佔位符的<bean>元素標籤進行解析,得到最終的配置值,這意味對一些半成品式的BeanDefinition對象進行加工處理並得到成品的BeanDefinition對象。
3.2 對BeanDefinitionRegistry中的BeanDefinition進行掃描,通過Java反射機制找出所有屬性編輯器的Bean(實現java.beans.PropertyEditor接口的Bean),並自動將它們註冊到Spring容器的屬性編輯器註冊表中(PropertyEditorRegistry)。
- Spring容器從BeanDefinitionRegistry中取出加工後的BeanDefinition,並調用InstantiationStrategy着手進行Bean實例化的工作;
- 在實例化Bean時,Spring容器使用BeanWrapper對Bean進行封裝,BeanWrapper提供了很多以Java反射機制操作Bean的方法,它將結合該Bean的BeanDefinition以及容器中屬性編輯器,完成Bean屬性的設置工作。
- 利用容器中註冊的Bean後處理器(實現BeanPostProcessor接口的Bean)對已經完成屬性設置工作的Bean進行後續加工,直接裝配出一個準備就緒的Bean。
1.3 Spring中的組件
Spring中的組件按照所承擔的角色可以劃分爲兩類:
- 在Bean創建過程中被處理的元素:Resource、BeanDefinition、PropertyEditor以及最終的Bean。
- 處理上述元素的工具類:ResourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy、BeanWrapper等。
1.4 BeanDefinition
-
org.springframework.beans.factory.config.BeanDefinition
是配置文件<bean>元素標籤在容器中的內部表示,是與<bean>一一對應的。 - 一般的<bean>和父<bean>用RootBeanDefinition表示,而子<bean>用ChildBeanDefinition表示。
- 一般情況下,BeanDefinition只在容器啓動時加載並解析,除非容器重啓或刷新。當然用戶也可以在運行時通過編程調整BeanDefinition的定義。
創建BeanDefinition主要包括兩個步驟:
- 利用BeanDefinitionReader讀取承載配置信息的Resource,通過XML解析器解析配置信息的DOM對象,簡單地每個<bean>生成對應地BeanDefinition對象。但是這裏生成的BeanDefinition可能是半成品,因爲在配置文件中,可能通過佔位符變量引用外部屬性文件的屬性,這些佔位符變量在這一步裏還沒有被解析出來。
- 利用容器中註冊的BeanFactoryPostProcessor對半成品的BeanDefinition進行加工處理,將以佔位符表示的配置解析爲最終的實際值,這樣半成品的BeanDefinition就成爲成品的BeanDefinition。
1.5 InstantiationStrategy
-
org.springframework.beans.factory.support.InstantiationStrategy
負責根據BeanDefinition對象創建一個Bean實例。 - InstantiationStrategy僅負責實例化Bean(相當於new的操作),不會設置的Bean屬性,所以InstantiationStrategy返回的並不是最終的Bean實例,還需要通過BeanWrapper進行屬性的設置。
- SimpleInstantiationStrategy是最常用的實例化策略,通過使用Bean的默認構造方法、帶參數的構造方法或工廠方法創建Bean的實例。
- CglibSubclassingInstantiationStrategy利用CGLib類庫爲Bean動態生成子類,在子類中生成方法注入的邏輯,然後使用這個動態生成的子類創建Bean的實例。
1.6 BeanWrapper
- BeanWrapper相當於一個代理器,Spring委託BeanWrapper完成Bean屬性填充工作。
- PropertyAccessor:屬性訪問接口定義了各種訪問Bean屬性的方法,如getPropertyValue、setPropertyValue等。
- PropertyEditorRegistry:是屬性編輯器的註冊表,主要作用就是註冊和保存屬性編輯器。
- BeanWrapperImpl:一個BeanWrapperImpl實例內部封裝了兩類組件——被封裝的待處理的Bean和一套用於設置Bean屬性的屬性編輯器。BeanWrapperImpl的三重身份——Bean的包裹器、屬性訪問器和屬性編輯器註冊表。
- Spring首先從容器的BeanDefinitionRegistry中獲取對應的BeanDefinition,然後從BeanDefinition中獲取Bean屬性的配置信息PropertyValue,然後使用屬性編輯器對PropertyValue進行轉換以得到Bean的屬性值。
2 屬性編輯器
我們在配置文件中配置的都是字面值,如果把它們轉換成對應數據類型(如double、int)的值或對象呢?
2.1 JavaBean的屬性編輯器
任何實現了java.beans.PropertyEditor
接口的類都是屬性編輯器,其主要功能就是將外部的設置值轉換成JVM內部的對應類型。
PropertyEditor
PropertyEditor是屬性編輯器的接口,它規定了將外部設置值轉換爲內部JavaBean屬性值的接口方法,是內部屬性值和外部設置值的橋樑。
- Object getValue():返回屬性的當前值。基本類型被封裝成對應的封裝類實例。
- void setValue(Object newValue):設置屬性的值,基本類型以封裝類傳入。
- String getAsText():將屬性對象用一個字符串表示,以便外部的屬性編輯器能以可視化的方式顯示。缺省返回null,表示該屬性不能以字符串表示。
- void setAsText(String text):用一個字符串去更新屬性的內部值,這個字符串一般從外部屬性編輯器傳入。
- String[] getTags():返回表示有效屬性值的字符串數組(如boolean屬性對應的有效Tag爲true和false),以便屬性編輯器能以下拉框的方式顯示出來。缺省返回null,表示屬性沒有匹配的字符值有限集合。
- String getJavaInitializationString():爲屬性提供一個表示初始值的字符串,屬性編輯器以此值作爲屬性的默認值。
- 我們一般不去直接實現PropertyEditor,而是擴展PropertyEditorSupport來實現自己類。
BeanInfo
BeanInfo主要描述了JavaBean的哪些屬性可以編輯及對應的屬性編輯器。BeanInfo和JavaBean的對應關係通過二者命名規範確定:對應JavaBean的BeanInfo的命名規範應該是<Bean>BeanInfo
,如Car對應的BeanInfo爲CarBeanInfo。
- JavaBean的每個屬性對應一個屬性描述器PropertyDescriptor。
- BeanInfo最重要的方法就是PropertyDescriptor[] getPropertyDescriptors(),該方法返回JavaBean的屬性描述數組。
- BeanInfo接口常用其實現類SimpleBeanInfo,可以擴展此類實現功能。
PropertyEditorManager
JavaBean規範提供了一個默認的屬性編輯器PropertyEditorManager,保存一些常見類型的屬性編輯器。
2.2 Spring屬性編輯器
Spring爲常見的屬性類型提供了默認的屬性編輯器PropertyEditorRegistrySupport,裏邊有多個用於保存屬性編輯器的Map類型變量,鍵爲屬性類型,值爲對應的屬性編輯器實例。常見的類型如下所示。
類 別 | 說 明 |
---|---|
基本數據類型 | 如:boolean、byte、short、int等; |
基本數據類型封裝類 | 如:Long、Character、Integer等; |
兩個基本數據類型的數組 | char[]和byte[]; |
大數類 | BigDecimal和BigInteger |
集合類 | 爲5種類型的集合類Collection、Set、SortedSet、List和SortedMap提供了編輯器 |
資源類 | 用於訪問外部資源的8個常見類Class、Class[]、File、InputStream、Locale、Properties、Resource[]和URL |
2.3 自定義屬性編輯器
Step1:我們可以通過擴展java.beans.PropertyEditorSupport類,並覆蓋其中的setAsText()方法即可自定義屬性編輯器。
class KFCWaitress {
private KFCCombo kfcCombo;
// getters & setters
}
class KFCCombo {
private String burger;
private String drink;
// getters & setters
}
/**
* KFCCombo的Editor
*/
class KFCComboEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
// 將字面值轉換爲屬性類型對象
String[] textArr = text.split(",");
KFCCombo kfcCombo = new KFCCombo();
kfcCombo.setBurger(textArr[0]);
kfcCombo.setDrink(textArr[1]);
// 調用父類的setValue()方法設置轉換後的屬性對象
setValue(kfcCombo);
}
}
Step2:如果使用BeanFactory需要手動調用registerCustomEditor(class requiredType, PropertyEditor propertyEditor)
方法註冊自定義的屬性編輯器;如果使用ApplicationContext,只需要在配置文件中通過CustomEditorConfigurer註冊即可。
<!-- (1)配置自動註冊屬性編輯器的CustomEditorConfigurer -->
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<!-- (2)屬性編輯器對應的屬性類型 -->
<entry key="com.ankeetc.spring.KFCCombo" value="com.ankeetc.spring.KFCComboEditor"/>
</map>
</property>
</bean>
<bean id="myWaitress" class="com.ankeetc.spring.KFCWaitress">
<!-- (3)該屬性將使用(2)處的屬性編輯器完成屬性填充操作 -->
<property name="kfcCombo" value="Zinger Burger,Mirinda"/>
</bean>
在(3)處,直接通過一個字符串配置一個Bean。BeanWrapper在設置KFCCombo類型的屬性時,將會檢索自定義屬性編輯器的註冊表,如果發現有KFCCombo屬性類型有對應的屬性編輯器時,就會使用該方法的setAsText()
轉換該對象。
3 使用外部屬性文件
Spring提供了一個PropertyPlaceholderConfigurer來引用外部屬性文件,它實現了BeanFactoryPostProcessor接口,因此也是一個Bean工廠後處理器。
3.1 使用PropertyPlaceholderConfigurer
簡單的例子
通過PropertyPlaceholderConfigurer並引入屬性文件,實現使用屬性名來引用屬性值。
<!-- 創建PropertyPlaceholderConfigurer的Bean並引入屬性文件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:application.properties"/>
<property name="fileEncoding" value="utf-8"/>
</bean>
<!-- 也可以使用這種方式引用屬性文件 -->
<context:property-placeholder location="classpath:application.properties" file-encoding="utf-8"/>
<!-- 通過屬性名引用屬性值 -->
<bean id="myCat" class="com.ankeetc.spring.Cat">
<property name="name" value="${name}"/>
</bean>
PropertyPlaceholderConfigurer的其他屬性
- location:如果只有一個屬性文件,則直接使用location屬性指定就可以了;如果有多個屬性文件,則可以通過locations屬性進行設置。可以像配置List一樣配置locations屬性。
- fileEncoding:屬性文件的編碼格式。Spring使用操作系統默認編碼讀取屬性文件。如果屬性文件採用了特殊編碼,則需要通過該屬性顯示指定。
- order:如果配置文件中定義了多個PropertyPlaceholderConfigurer,則通過該屬性指定優先順序。
- placeholderPrefix:在上面的例子中,通過
${屬性名}
引用屬性文件中的屬性項,其中${
爲默認的佔位符前綴,可以根據需要改爲其他的前綴符。 - placeholderSuffix:佔位符後綴,默認爲
}
。
@Value引用屬性
在使用基於註解配置Bean時,可以通過@Value註解爲Bean的成員變量或方法入參自動注入容器中已有的屬性,也可以使用@Value注入字面值。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring);
System.out.println(applicationContext.getBean(Cat.class).getName());
}
}
@Configuration
class Config {
@Bean
public PropertyPlaceholderConfigurer configurer() {
PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
configurer.setLocation(new PathMatchingResourcePatternResolver().getResource("classpath:application.properties"));
configurer.setFileEncoding("utf-8");
return configurer;
}
}
@Component
class Cat {
@Value("${name}")
private String name;
public String getName() {
return name;
}
}
3.2 使用加密的屬性文件
如果屬性是敏感的,一般不允許使用明文形式保存,此時需要對屬性進行加密.PropertyPlaceHolderConfigurer繼承自PropertyResourceConfigurer類,後者有幾個有用的protected方法(方法默認是空的即不會轉換),用於在屬性使用之前對屬性列表中的屬性進行轉換。
-
void convertProperties(Properties props)
:屬性文件的所有屬性值都封裝在props中,覆蓋該方法,可以對所有的屬性值進行轉換處理。 -
String convertProperty(String propertyName, String propertyValue)
:在加載屬性文件並讀取文件中的每個屬性時,都會調用此方法進行轉換處理。 -
String convertPropertyValue(String originalValue)
:和上一個方法類似,只不過沒有傳入屬性名。
簡單例子
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.ankeetc.spring");
// userName沒有被改變
System.out.println(applicationContext.getBean(DataSource.class).getUserName());
// password值被改變
System.out.println(applicationContext.getBean(DataSource.class).getPassword());
}
}
@Component
class DataSource {
@Value("${userName}")
private String userName;
@Value("${password}")
private String password;
public String getUserName() {
return userName;
}
public String getPassword() {
return password;
}
}
@Configuration
class Config {
@Bean
public EncryptPropertyPlaceholderConfigurer encryptPropertyPlaceholderConfigurer() {
EncryptPropertyPlaceholderConfigurer configurer = new EncryptPropertyPlaceholderConfigurer();
configurer.setLocation(new PathMatchingResourcePatternResolver().getResource("classpath:application.properties"));
configurer.setFileEncoding("utf-8");
return configurer;
}
}
class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if ("password".equals(propertyName)) {
// 在此過濾並實現相關的揭祕邏輯
return "Decrypt" + propertyValue;
} else {
return propertyValue;
}
}
}
3.3 屬性文件
- 可以在屬性文件中使用
${}
來實現屬性之間的相互引用
dbName=myDatabase
url=jdbc:mysql://localhost:3306/${dbName}
- 如果一個屬性值太長,可以在行後添加
\
將屬性分爲多行
4 應用Bean的屬性值
基於XML的配置
在XML配置文件中,可以使用#{beanName.propName}
的方式引用其他Bean的屬性值。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
System.out.println(applicationContext.getBean(Application.class).getDatabaseName());
System.out.println(applicationContext.getBean(Application.class).getDatabasePassword());
}
}
class DatabaseConfig {
private String userName;
private String password;
// getters & setters
}
class Application {
private String databaseName;
private String databasePassword;
// getters & setters
}
<bean id="databaseConfig" class="com.ankeetc.spring.DatabaseConfig">
<property name="userName" value="lucas"/>
<property name="password" value="123456"/>
</bean>
<!--通過#{databaseConfig.propName}的方式引用databaseConfig的屬性值-->
<bean id="applicationConfig" class="com.ankeetc.spring.Application">
<property name="databaseName" value="#{databaseConfig.userName}"/>
<property name="databasePassword" value="#{databaseConfig.password}"/>
</bean>
基於註解和Java類的配置
使用@Value("#{beanName.propName}")
的形式也可以引用其他類的屬性值。
@Component
class Application {
@Value("#{databaseConfig.userName}")
private String databaseName;
@Value("#{databaseConfig.password}")
private String databasePassword;
}
5 國際化信息
國際化信息的含義是根據不同的地區語言類型返回不同的信息,簡單來說就是爲每種語言配置一套對應的資源文件。
5.1 基礎知識
本地化類java.util.Locale
國際化信息也稱爲本地化信息,由java.util.Locale
類表示一個本地化對象。它由語言參數和國家/地區參數構成。
- 語言參數:每種語言由兩位小寫字母表示,如
zh
、en
- 國家/地區參數:用兩個大寫字母表示,如
CN
、TW
、HK
、EN
、US
本地化工具類
java.util包下的NumberFormat、DateFormat、MessageFormat都支持本地化的格式化操作,而且MessageFormat還支持佔位符的格式化操作。
ResourceBundle
使用ResourceBundle可以訪問不同本地化資源文件,文件名必須按照如下格式來命名:資源名_語言代碼_國家地區代碼.properties
,其中語言代碼和國家/地區代碼是可選的。假如默認資源文件的文件名爲application.properties
,則中文中國大陸的資源文件名爲application_zh_CN.properties
。
public class Main {
public static void main(String[] args) {
// 如果找不到對應的資源文件,將會使用默認的資源文件
// getBundle是類路徑的文件名,而且不帶.properties後綴
ResourceBundle zhCN = ResourceBundle.getBundle("application", Locale.SIMPLIFIED_CHINESE);
ResourceBundle enUs = ResourceBundle.getBundle("application", Locale.US);
System.out.println(zhCN.getString("name"));
System.out.println(enUs.getString("name"));
}
}
5.2 MessageSource
Spring定義了訪問國際化信息的MessageSource接口,主要方法如下:
-
String getMessage(String code, Object[] args, String defaultMessage, Locale locale)
:code表示國際化資源中的屬性名;args用於傳遞格式化串佔位符所用的運行期參數;當在資源找不到對應屬性名時,返回defaultMessage參數所指定的默認信息;locale表示本地化對象; -
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException
:與上面的方法類似,只不過在找不到資源中對應的屬性名時,直接拋出NoSuchMessageException異常; -
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException
:MessageSourceResolvable將屬性名、參數數組以及默認信息封裝起來,它的功能和第一個接口方法相同。
類結構
-
HierarchicalMessageSource
接口的作用是建立父子層級的MessageSource結構。 -
StaticMessageSource
主要用於程序測試,它允許通過編程的方式提供國際化信息。 -
ResourceBundleMessageSource
實現類允許通過beanName指定一個資源名(包括類路徑的全限定資源名),或通過beanNames指定一組資源名。 -
ReloadableResourceBundleMessageSource
提供了定時刷新功能,允許在不重啓系統的情況下,更新資源的信息。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
System.out.println(applicationContext.getBean("myMessageSource1", MessageSource.class).getMessage("name", null, Locale.US));
System.out.println(applicationContext.getBean("myMessageSource1", MessageSource.class).getMessage("name", null, Locale.SIMPLIFIED_CHINESE));
}
}
<!--可以使用basename指定資源文件的路徑-->
<bean id="myMessageSource1" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="application"/>
</bean>
<!--可以使用basenames指定多組資源文件的路徑-->
<bean id="myMessageSource2" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>application</value>
</list>
</property>
</bean>
<!--當使用ReloadableResourceBundleMessageSource可以使用cacheSeconds指定刷新週期-->
<!--刷新週期默認爲-1即不刷新,單位是秒-->
<bean id="myMessageSource3" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="application"/>
<property name="cacheSeconds" value="100"/>
</bean>
5.3 容器級的國際化信息
由於ApplicationContext本身也繼承了MessageSource接口,所以ApplicationContext的所有實現類本身也是一個MessageSource對象,國際化信息是整個容器的公共設施。
在本章(1.1 ApplicationContext內部原理)我們提到,在ApplicationContext會在initMessageSource()方法中,Spring通過反射機制找出bean名爲messageSource
(bean名必須是messageSource)且類型爲MessageSource子類的Bean,將這個Bean定義的信息資源加載爲容器級的國際化信息資源。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
System.out.println(applicationContext.getMessage("name", null, Locale.US));
}
}
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="application"/>
</bean>
6 容器事件
6.1 Java的事件體系
事件體系是觀察者模式的一種具體實現方式,一共有如下幾個角色:
- 事件:
java.util.EventObject
是Java中的事件。 - 監聽器:
java.util.EventListener
是用於描述事件的接口,是一個沒有任何方法的標記接口。 - 事件源:事件的生產者,任何一個EventObject都有一個事件源。
- 事件監聽器註冊表:框架必須有一個地方來保存事件監聽器,當事件源產生事件時,就會通知這些位於註冊表中的監聽器。
- 事件廣播器:是事件和事件監聽器之間的橋樑,負責把事件通知給事件監聽器。
public class Main {
public static void main(String[] args) {
Waitress waitress = new Waitress("田二妞");
waitress.addEventListener(new Chef("王二狗"));
waitress.order("宮保雞丁");
// 廚師[王二狗]收到服務員[田二妞]的訂單,開始做[宮保雞丁]
}
}
// 一個餐廳的點單事件,繼承了EventObject
class OrderEventObject extends EventObject {
private String order;
public String getOrder() {
return order;
}
public OrderEventObject(Object source, String order) {
super(source);
this.order = order;
}
}
// 服務員是事件源,由她產生點單事件
class Waitress {
private String name;
// 服務員維護了所有在餐廳的廚師,即監聽器註冊表
private List<Chef> eventListenerList = new ArrayList<>();
public Waitress(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void addEventListener(Chef chef) {
eventListenerList.add(chef);
}
// 該方法是廣播器,即把點單事件通知給註冊表中的全部廚師
public void order(String order) {
OrderEventObject orderEventObject = new OrderEventObject(this, order);
eventListenerList.forEach(var -> var.cook(orderEventObject));
}
}
// 廚師是事件監聽器
class Chef implements EventListener {
private String name;
public Chef(String name) {
this.name = name;
}
// 監聽到點單事件並作出相關反應
public void cook(EventObject o) {
System.out.println(String.format("廚師[%s]收到服務員[%s]的訂單,開始做[%s]", name, ((Waitress)o.getSource()).getName(), ((OrderEventObject)o).getOrder()));
}
}
6.2 Spring事件類結構
事件類
- ApplicationEvent:Spring的事件類的基類,其類結構如下所示。
- ApplicationContextEvent:容器事件,它擁有4個子類分別表示容器的啓動、刷新、停止、關閉事件。
- RequestHandleEvent:與Web應用有關的事件,當一個HTTP請求被處理後產生該事件。只有在web.xml中定義了DispatcherServlet時纔會產生該事件。它有兩個子類,分別代表Servlet和Portlet的請求事件。
事件監聽器接口
- ApplicationListener:該接口只定義了一個方法
onApplicationEvent(E event)
,該方法接受ApplicationEvent事件對象,在該方法中編寫事件的響應處理邏輯。 - SmartApplicationListener:定義了兩個方法
boolean supportsEventType(Class<? extends ApplicationEvent> eventType)
:指定監聽器支持哪種類型的容器事件,即它只會對該類型的事件做出響應;boolean supportsSourceType(Class<?> sourceType)
:指定監聽器僅對何種事件源對象做出響應。 - GenericApplicationListener:Spring 4.2新增的類,使用可解析類型ResolvableType增強了對範型的支持。
事件廣播器
當發生容器事件時,容器主控程序將調用事件廣播器將事件通知給事件監聽器註冊表中的事件監聽器。Spring爲事件廣播器提供了接口和實現類。
6.3 Spring事件體系具體實現
Spring在ApplicationContext接口的抽象實現類AbstractApplicationContext中完成了事件體系的搭建。AbstractApplicationContext擁有一個applicationEventMulticaster(應用上下文事件廣播器)成員變量,它提供了容器監聽器的註冊表。AbstractApplicationContext在refresh()這個容器啓動啓動方法中通過以下3個步驟搭建了事件的基礎設施:
public void refresh() throws BeansException, IllegalStateException {
// (5)初始化應用上下文事件廣播器
initApplicationEventMulticaster();
// (7)註冊事件監聽器
registerListeners();
// (9)完成刷新併發布容器刷新事件
finishRefresh();
}
- 在(5)處,Spring初始化事件的廣播器,可以在配置文件中爲容器定義一個自定義的事件廣播器,只要實現ApplicationEventMulticaster即可,Spring會通過反射機制將其註冊容器的事件廣播器。如果沒有找到配置的外部事件廣播器,則Spring自動使用SimpleApplicationEventMulticaster作爲事件廣播器。
- 在(7)處,Spring根據反射機制,從BeanDefinitionRegistry中找出所有實現ApplicationListener的Bean,將它們註冊爲容器的事件監聽器,即將其添加到事件廣播器所提供的事件監聽器註冊表中
- 在(9)處,容器啓動完成,調用事件發佈接口向容器中所有的監聽器發佈事件
6.4 一個例子
假如我們希望容器刷新時打印一行文字,可以繼承GenericApplicationListener並實現相關方法。
public class Main {
public static void main(String[] args) {
// new AnnotationConfigApplicationContext()會調用refresh方法,MyListener會監聽到並處理
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.ankeetc.spring");
// stop事件不會被監聽到
((AnnotationConfigApplicationContext) applicationContext).stop();
}
}
@Component
class MyListener implements GenericApplicationListener {
// 判斷是否是刷新事件
@Override
public boolean supportsEventType(ResolvableType eventType) {
return ResolvableType.forClass(ContextRefreshedEvent.class).equals(eventType);
}
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
// 在此實現監聽到相關事件的處理邏輯
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("Hello world");
}
@Override
public int getOrder() {
return 0;
}
}