spring 系列 轉載自掘金 VipAugus https://juejin.cn/user/2348212565601415/posts
在上一篇文章中悠砚,深入分析和學習了 BeanFactoryPostProcessor
扒披,主體是 BeanFactory
的后處理器堰酿,這次來學習主體是 Bean
的后處理器:BeanPostProcessor
。
定義:它也是 Spring 對外提供的接口后添,用來給用戶擴展自定義的功能。執(zhí)行的時機在 bean 實例化階段前后
本篇思路:
-
BeanPostProcessor
定義 - 如何使用
- 代碼實現(xiàn)分析
- 介紹剩余的擴展功能
前言
與 BeanFactoryPostProcessor
不同的是薪丁,BeanFactoryPostProcessor
的注冊和執(zhí)行都在同一個方法內遇西,而 BeanPostProcessor
分開兩個方法馅精,分為注冊和調用兩個步驟。
常規(guī)的 BeanFactory 中是沒有實現(xiàn)后處理器的自動注冊粱檀,所以在調用的時候沒有進行手動注冊是無法使用的洲敢,但在 ApplicationContext 中添加了自動注冊功能(在這個 registerBeanPostProcessors 方法中),最后在 bean 實例化時執(zhí)行 BeanPostProcessor 對應的方法茄蚯。
本次主要介紹 BeanPostProcessor
压彭,同時也會將剩下的 context
擴展功能一起學習~
BeanPostProcessor
經過上一篇文章的學習,應該對 bean
的后處理理解起來更順利渗常,下面直奔主題壮不,來看下它是如何使用和結合源碼分析
如何使用
新建一個 bean 后處理器
這個后處理器需要引用 InstantiationAwareBeanPostProcessor
接口(實際繼承自 BeanPostProcessor
),然后重載以下兩個方法:
public class CarBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 這里沒有區(qū)分 bean 類型皱碘,只是用來測試打印的順序和時間
System.out.println("Bean name : " + beanName + ", before Initialization, time : " + System.currentTimeMillis());
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean name : " + beanName + ", after Initialization, time : " + System.currentTimeMillis());
return null;
}
}
復制代碼
在配置文件中注冊 bean-post-processor.xml
在配置文件配置我們寫的自定義后處理器和兩個普通 bean
询一,用來測試打印時間和順序
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- beanPostProcessor -->
<bean id="carPostProcessor" class="context.bean.CarBeanPostProcessor"/>
<!--用以下兩個 bean 進行測試打印時間和順序-->
<bean id="car" class="base.factory.bean.Car">
<property name="price" value="10000"/>
<property name="brand" value="奔馳"/>
</bean>
<bean id="book" class="domain.ComplexBook"/>
</beans>
復制代碼
啟動代碼和打印結果
public class CarBeanPostProcessorBootstrap {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("factory.bean/bean-post-processor.xml");
Car car = (Car) context.getBean("car");
ComplexBook book = (ComplexBook) context.getBean("book");
System.out.println(car);
System.out.println(book);
}
}
復制代碼
輸出:
Bean name : car, before Initialization, time : 1560772863996
Bean name : car, after Initialization, time : 1560772863996
Bean name : book, before Initialization, time : 1560772863999
Bean name : book, after Initialization, time : 1560772863999
Car{maxSpeed=0, brand='奔馳', price=10000.0}
domain.ComplexBook@77be656f
復制代碼
從輸出接口看出,打印順序是先框架內部癌椿,再到應用層健蕊,框架內部中,在順序實例化每個 bean
時踢俄,前面也提到執(zhí)行時機:先執(zhí)行 postProcessBeforeInitialization 方法绊诲,然后實例化 bean 后,執(zhí)行 postProcessAfterInitialization褪贵。
所以我們重載的兩個接口按照前后順序打印出來了~
注冊 BeanPostProcessor
上面介紹了使用例子掂之,應該不難理解,接著來看下源碼注冊的方法:
org.springframework.context.support.AbstractApplicationContext#registerBeanPostProcessors
實際委托給了 PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
// 注釋 7.2 從注冊表中取出 class 類型為 BeanPostProcessor 的 bean 名稱列表
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));
// 將帶有 權限順序脆丁、順序和其余的 beanPostProcessor 分開
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
// 類型是 MergedBeanDefinitionPostProcessor
List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
// 分類世舰,添加到對應數(shù)組中
...
}
// 首先,注冊實現(xiàn)了 PriorityOrdered 接口的 bean 后處理器
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);
// 下一步槽卫,注冊實現(xiàn)了 Ordered 接口的 bean 后處理器
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);
// 現(xiàn)在跟压,注冊常規(guī) bean 后處理器,其實就是不帶順序
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);
// 最后歼培,重新注冊 MergedBeanDefinitionPostProcessor 類型的后處理器
// 看起來是重復注冊了震蒋,但是每次注冊調用的底層方法都會先移除已存在的 beanPostProcessor,然后再加進去躲庄,最后還是保存唯一
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);
// 添加 ApplicationContext 探測器
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}
復制代碼
跟之前的 BeanFactoryPostProcessor
處理是不是很相似查剖,也是進行分類,將帶有權重順序噪窘、順序和普通 BeanPostProcessor
添加到對應的列表后笋庄,然后排序,統(tǒng)一注冊到 beanPostProcessors
列表末尾。
將 BeanPostProcessor
與之前的 BeanFactoryPostProcessor
進行對比后發(fā)現(xiàn)直砂,少了硬編碼注冊的代碼菌仁,只處理了配置文件方式的注冊 bean
。通過書中闡釋静暂,對少了硬編碼的處理有些理解:
對于 BeanFactoryPostProcessor 的處理济丘,在一個方法內實現(xiàn)了注冊和實現(xiàn),所以需要載入配置中的定義洽蛀,并進行激活闪盔;而對于 BeanPostProcessor 并不需要馬上調用,硬編碼的方式實現(xiàn)的功能是將后處理器提取并調用辱士,對于 BeanPostProcessor泪掀,注冊階段不需要調用,所以沒有考慮處理硬編碼颂碘,在這里只需要將配置文件的 BeanPostProcessor 提取出來并注冊進入 beanFactory 就可以了异赫。
而且我在測試過程,想在應用代碼中進行硬編碼注冊头岔,發(fā)現(xiàn)由于 ClassPathXmlApplicationContext 最后一個方法是實例化非延遲加載的 bean塔拳,在上下文創(chuàng)建好時,BeanPostProcessor 就已經執(zhí)行完成了峡竣,于是硬編碼注冊的后處理器無法執(zhí)行靠抑,只能通過設定延遲加載或者在配置文件配置中進行注冊,或者其它 BeanFactory 能支持硬編碼适掰。
剩下順序 Order
類型的后處理器注冊 BeanFactoryPostProcessor
類似就不重復多講解了颂碧,這段代碼的邏輯挺清晰的~
小結
結束兩個擴展功能,BeanFactoryPostProcessor
和 BeanPostProcessor
的學習使用后类浪,還有其它的擴展功能沒學習到载城,在一開始基礎機構篇就提到剩下的方法:
這這些擴展功能中,個人感覺事件傳播器费就、監(jiān)聽器和發(fā)送廣播事件這三個會用得比較多诉瓦,所以下面的內容會花比較大篇幅講這三個擴展。
初始化消息資源
根據(jù)書中的內容介紹力细,這個消息資源 messageSource
是跟 Spring
國際化相關睬澡。
例如中美之間的中英文差別,在不同地區(qū)顯示不同的資源眠蚂。對于有國際化需求的系統(tǒng)煞聪,要為每種提供一套相應的資源文件,并以規(guī)范化命名的形式保存在特定的目錄中河狐,由系統(tǒng)自動根據(jù)客戶端的語言或者配置選擇合適的資源文件米绕。
舉個??: 定義了兩個資源文件瑟捣,簡單配置如下
- 中文地區(qū): test=測試
- 英文地區(qū): test=test
所以可以通過 Applicationcontext.getMessage() 方法訪問國際化信息馋艺,在不同的環(huán)境中獲取對應的數(shù)據(jù)栅干。
由于個人感覺這種配置相關的,可以通過 profile
切換來實現(xiàn)捐祠,所以沒有去細看和使用碱鳞,具體實現(xiàn)和使用請感興趣的同學們深入了解吧。
事件監(jiān)聽
事件傳播器的使用很像我們設計模式中的觀察者模式踱蛀,被觀察者變動后通知觀察者進行相應的邏輯處理窿给。
在了解 Spring
如何初始化事件傳播器之前,來看下 Spring
監(jiān)聽的簡單用法率拒。
定義監(jiān)聽事件 Event
新建一個類崩泡,繼承于 ApplicationEvent
,并且需要在構造方法中調用父類的構造函數(shù) supre(source)
:
public class CarEvent extends ApplicationEvent {
/**
* 自定義一個消息
*/
private String msg;
public CarEvent(Object source) {
super(source);
}
public CarEvent(Object source, String msg) {
super(source);
this.msg = msg;
}
}
復制代碼
定義監(jiān)聽器 Listener
新建一個類猬膨,引用 ApplicationListener
接口角撞,然后重載 onApplicationEvent
方法:
public class CarEventListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof CarEvent) {
CarEvent carEvent = (CarEvent) event;
System.out.println("source : " + event.getSource() + ", custom message : " + carEvent.getMsg());
}
}
}
復制代碼
由于 Spring
的消息監(jiān)聽器不像 kafka
等主流 MQ
可以指定發(fā)送隊列或者監(jiān)聽主題,只要發(fā)送消息后勃痴,所有注冊的監(jiān)聽器都會收到消息進行處理谒所,所以這邊加了一個判斷,如果是我業(yè)務上需要的消息沛申,才會進行處理劣领。
配置文件
<bean id="testListener" class="context.event.CarEventListener"/>
復制代碼
將剛才寫的監(jiān)聽器注冊到 Spring
容器中
測試代碼
public class EventBootstrap {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("factory.bean/bean-post-processor.xml");
// 第一個參數(shù)是來源,第二個參數(shù)是自定義
CarEvent carEvent = new CarEvent("hello", "world");
context.publishEvent(carEvent);
// 消息發(fā)送之后铁材,打印以下內容
// source : hello, custom message : world
}
}
復制代碼
由于在配置文件中注冊了監(jiān)聽器尖淘,然后在啟動代碼匯總初始化了監(jiān)聽事件,最終通過 context
發(fā)送消息著觉,發(fā)現(xiàn)輸出結果與預想的一致德澈。
這種觀察者模式實現(xiàn)很經典,使用起來也很簡單固惯,下面來結合源碼分析一下 Spring
是如何實現(xiàn)消息監(jiān)聽的功能梆造。
消息監(jiān)聽代碼分析
從源碼中分析,發(fā)現(xiàn)主要是下面三個步驟:
初始化 ApplicationEvenMulticaster
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 如有有自己注冊class Name 是 applicationEventMulticaster葬毫,使用自定義廣播器
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
}
}
else {
// 沒有自定義镇辉,使用默認的事件廣播器
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
}
}
復制代碼
廣播器的作用是用來廣播消息,在默認的廣播器 SimpleApplicationEventMulticaster
類中發(fā)現(xiàn)了這個方法 multicastEvent
:
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
// 遍歷注冊的消息監(jiān)聽器
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
復制代碼
可以看到贴捡,在廣播事件時忽肛,會遍歷所有注冊的監(jiān)聽器進行調用 invokeListener 方法,底層調用的是監(jiān)聽器重載的 listener.onApplicationEvent(event)烂斋,所以再次強調一次屹逛,如果使用 Spring 自帶的事件監(jiān)聽础废,請在業(yè)務處理方判斷事件來源,避免處理錯誤罕模。
注冊監(jiān)聽器
在上一步中评腺,已經初始化好了廣播器,所以下一步來看下淑掌,監(jiān)聽器的注冊流程蒿讥,入口方法如下:
org.springframework.context.support.AbstractApplicationContext#registerListeners
protected void registerListeners() {
// 這里是硬編碼注冊的監(jiān)聽器
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
// 不要在這里初始化 factoryBean : 我們需要保留所有常規(guī) bean 未初始化,以便讓后處理程序應用于它們!
// 這一步是配置文件中注冊的監(jiān)聽器
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
// 發(fā)布早期的應用程序事件抛腕,現(xiàn)在我們終于有了一個多播器=-=
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
復制代碼
這一個方法代碼不多芋绸,也沒啥嵌套功能,按照注釋順序將流程梳理了一遍担敌,將我們注冊的監(jiān)聽器加入到 applicationEventMulticaster
列表中摔敛,等待之后調用。
publishEvent
廣播器和監(jiān)聽器都準備好了全封,剩下的就是發(fā)送事件马昙,通知監(jiān)聽器做相應的處理:
org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)
核心是這行代碼:
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
通過獲取事件廣播器,調用 multicastEvent
方法售貌,進行廣播事件给猾,這一步前面也介紹過了,不再細說颂跨。
總結
這次學習敢伸,省略了書中的一些內容,有關屬性編輯器恒削、SPEL
語言和初始化非延遲加載等內容池颈,請感興趣的同學繼續(xù)深入了解~
我們也能從 Spring 提供的這些擴展功能中學習到,通過預留后處理器钓丰,可以在 bean 實例化之前修改配置信息躯砰,或者做其他的自定義操作,例如替換占位符携丁、過濾敏感信息等琢歇;
也可以通過廣播事件,定義事件和監(jiān)聽器梦鉴,在監(jiān)聽器中實現(xiàn)業(yè)務邏輯李茫,由于不是直接調用監(jiān)聽器,而是通過事件廣播器進行中轉肥橙,達到了代碼解耦的效果魄宏。
所以在之后的代碼設計和編寫中,在整體設計上存筏,有必要的話宠互,考慮在更高的抽象層要預留擴展功能味榛,然后讓子類重載或者實現(xiàn),實現(xiàn)擴展的功能予跌。
由于個人技術有限搏色,如果有理解不到位或者錯誤的地方,請留下評論匕得,我會根據(jù)朋友們的建議進行修正
代碼和注釋都在里面继榆,小伙伴們可以下載我上傳的代碼巾表,親測可運行~
Gitee 地址:https://gitee.com/vip-augus/spring-analysis-note.git