Spring 源碼學習(七) 擴展功能 下篇-BeanPostProcessor

spring 系列 轉載自掘金 VipAugus https://juejin.cn/user/2348212565601415/posts

在上一篇文章中悠砚,深入分析和學習了 BeanFactoryPostProcessor 扒披,主體是 BeanFactory 的后處理器堰酿,這次來學習主體是 Bean 的后處理器:BeanPostProcessor

定義:它也是 Spring 對外提供的接口后添,用來給用戶擴展自定義的功能。執(zhí)行的時機在 bean 實例化階段前后

本篇思路:

  1. BeanPostProcessor 定義
  2. 如何使用
  3. 代碼實現(xiàn)分析
  4. 介紹剩余的擴展功能

前言

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 類似就不重復多講解了颂碧,這段代碼的邏輯挺清晰的~


小結

結束兩個擴展功能,BeanFactoryPostProcessorBeanPostProcessor 的學習使用后类浪,還有其它的擴展功能沒學習到载城,在一開始基礎機構篇就提到剩下的方法:

這這些擴展功能中,個人感覺事件傳播器费就、監(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

Github 地址:https://github.com/Vip-Augus/spring-analysis-note

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末汁掠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子集币,更是在濱河造成了極大的恐慌考阱,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞠苟,死亡現(xiàn)場離奇詭異乞榨,居然都是意外死亡,警方通過查閱死者的電腦和手機当娱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門吃既,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人跨细,你說我怎么就攤上這事鹦倚。” “怎么了冀惭?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵震叙,是天一觀的道長。 經常有香客問我散休,道長媒楼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任戚丸,我火速辦了婚禮划址,結果婚禮上,老公的妹妹穿的比我還像新娘限府。我一直安慰自己夺颤,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布谣殊。 她就那樣靜靜地躺著拂共,像睡著了一般。 火紅的嫁衣襯著肌膚如雪姻几。 梳的紋絲不亂的頭發(fā)上宜狐,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天势告,我揣著相機與錄音,去河邊找鬼抚恒。 笑死咱台,一個胖子當著我的面吹牛,可吹牛的內容都是我干的俭驮。 我是一名探鬼主播回溺,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼混萝!你這毒婦竟也來了遗遵?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤逸嘀,失蹤者是張志新(化名)和其女友劉穎车要,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崭倘,經...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡翼岁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了司光。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琅坡。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖残家,靈堂內的尸體忽然破棺而出榆俺,到底是詐尸還是另有隱情,我是刑警寧澤跪削,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布谴仙,位于F島的核電站,受9級特大地震影響碾盐,放射性物質發(fā)生泄漏晃跺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一毫玖、第九天 我趴在偏房一處隱蔽的房頂上張望掀虎。 院中可真熱鬧,春花似錦付枫、人聲如沸烹玉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽二打。三九已至,卻和暖如春掂榔,著一層夾襖步出監(jiān)牢的瞬間继效,已是汗流浹背症杏。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瑞信,地道東北人厉颤。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像凡简,于是被迫代替她去往敵國和親逼友。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內容