Spring事件監聽--源碼總結

Spring事件監聽

1.舉例

(1)定義監聽器監聽的對象BaseFetchDataEvent

@Getter
@Setter
@ToString
public class BaseFetchDataEvent extends ApplicationEvent {

    /** 測試數據對象 */
    private ObjectDto objectDto;

    /** 計數器 **/
    private CountDownLatch countDownLatch;

    /**
     * Create a new ApplicationEvent.
     *
     * @param source the object on which the event initially occurred (never {@code null})
     */
    public BaseFetchDataEvent(Object source) {
        super(source);
    }
}

(2)創建一個測試對象實體類

@Data
public class ObjectDto {

    /** 主鍵ID*/
    private Long id;

    /** 任務ID*/
    private Long taskId;

    /** status*/
    private Integer status;

}

(3)創建兩個監聽器,直接監聽BaseFetchDataEvent事件

@Slf4j
@Order(1)
@EasyService
public class OneBaseDataEventListener implements ApplicationListener<BaseFetchDataEvent> {

    @Override
    public void onApplicationEvent(BaseFetchDataEvent event) {
        if (event == null) return;

        ObjectDto dto = event.getObjectDto();
        CountDownLatch countDownLatch = event.getCountDownLatch();
        try {
            dto.setStatus(2);
        } catch (Exception e) {
            log.error("e:{}",e);
        }finally {
            countDownLatch.countDown();
        }
    }
}

@Slf4j
@Order(2)
@EasyService
public class TwoBaseDataEventListener implements ApplicationListener<BaseFetchDataEvent> {

    @Override
    public void onApplicationEvent(BaseFetchDataEvent event) {
        if (event == null) return;

        ObjectDto dto = event.getObjectDto();
        CountDownLatch countDownLatch = event.getCountDownLatch();
        try {
            dto.setId(1l);
            dto.setTaskId(1001l);
        } catch (Exception e) {
            log.error("e:{}",e);
        }finally {
            countDownLatch.countDown();
        }
    }
}

(4)定義測試類:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class)
@ActiveProfiles("dev")
@WebAppConfiguration
@Slf4j
public class ApplicationListenerTest {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Test
    public void listenerTest(){
        ObjectDto objectDto = new ObjectDto();
        try {
            CountDownLatch countDownLatch = new CountDownLatch(2);
            BaseFetchDataEvent fetchDataEvent = new BaseFetchDataEvent("基礎數據抓取事件");
            fetchDataEvent.setCountDownLatch(countDownLatch);
            fetchDataEvent.setObjectDto(objectDto);
            applicationEventPublisher.publishEvent(fetchDataEvent);
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(objectDto.toString());
    }
}

(5)執行結果

image

監聽器監聽到BaseFetchDataEvent事件,并調用onApplicationEvent方法

2源碼分析

ApplicationEventPublisher接口(封裝事件發布功能)提供了一個方法publishEvent,將事件發送出去,通知應用所有已注冊且匹配的監聽器此ApplicationEvent;通知應用所有已注冊且匹配的監聽器此Event ,如果這個Event不是一個ApplicationEvent,則其被包裹于PayloadApplicationEvent

[站外圖片上傳中...(image-f7841a-1551080194999)]

核心是:getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); //事件發布委托給ApplicationEventMulticaster來執行getApplicationEventMulticaster()方法是獲取所有的監聽器。

image

然后ApplicationEventMulticastermulticastEvent方法的實現在SimpleApplicationEventMulticaster類中:
獲取event所有的監聽事件,然后遍歷執行監聽器的onApplicationEvent方法,可知此方法是核心方法,是真正調用監聽器的地方;

從下面代碼可以看到,找到已注冊的ApplicationListener,逐個調用invokeListener方法,將ApplicationListener和事件作為入參傳進去就完成了廣播;

[站外圖片上傳中...(image-368b29-1551080194999)]
最終調用invokelistener,執行onApplicationEvent(event)invokeListener方法:,ApplicationListener是代表監聽的接口,只要調用這個接口的方法并且將event作為入參傳進去,那么每個監聽器就可以按需要自己來處理這條廣播消息了,
[站外圖片上傳中...(image-9f76b5-1551080194999)]

如果多線程同時發廣播,會不會有線程同步的問題?
唯一有可能出現問題的地方在:multicastEvent方法獲取ApplicationListener的時候可能出現同步問題,看代碼:

[站外圖片上傳中...(image-40aed3-1551080194999)]

protected Collection<ApplicationListener<?>> getApplicationListeners(
            ApplicationEvent event, ResolvableType eventType) {

    Object source = event.getSource();

    Class<?> sourceType = (source != null ? source.getClass() : null);
    //緩存的key有兩個維度:消息來源+消息類型(關于消息來源可見ApplicationEvent構造方法的入參)
    ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

    // retrieverCache是ConcurrentHashMap對象,所以是線程安全的,
    // ListenerRetriever中有個監聽器的集合,并有些簡單的邏輯封裝,
    //調用它的getApplicationListeners方法返回的監聽類集合是排好序的(order注解排序)
    ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
    if (retriever != null) {
        //如果retrieverCache中找到對應的監聽器集合,就立即返回了
        return retriever.getApplicationListeners();
    }

    if (this.beanClassLoader == null ||
       (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
                    (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
        //如果retrieverCache中沒有數據,就在此查出數據并放入緩存,
        //先加鎖
        synchronized (this.retrievalMutex) {
            //雙重判斷的第二重,避免自己在BLOCK的時候其他線程已經將數據放入緩存了
            retriever = this.retrieverCache.get(cacheKey);
            if (retriever != null) {
                return retriever.getApplicationListeners();
            }
            //新建一個ListenerRetriever對象
            retriever = new ListenerRetriever(true);
            //retrieveApplicationListeners方法復制找出某個消息類型加來源類型對應的所有監聽器
            Collection<ApplicationListener<?>> listeners =
                    retrieveApplicationListeners(eventType, sourceType, retriever);
            //存入retrieverCache  
            this.retrieverCache.put(cacheKey, retriever);
            //返回結果
            return listeners;
        }
    }
    else {
        // No ListenerRetriever caching -> no synchronization necessary
        return retrieveApplicationListeners(eventType, sourceType, null);
    }
}

在廣播消息的時刻,如果某個類型的消息在緩存中找不到對應的監聽器集合,就調用retrieveApplicationListeners方法去找出符合條件的所有監聽器,然后放入這個集合

3 如何具備消息發送能力

spring容器初始化的時候會對實現了Aware接口的bean做相關的特殊處理,其中就包含ApplicationEventPublisherAware這個與廣播發送相關的接口

image

在spring容器初始化的時候,AbstractApplicationContext類的prepareBeanFactory方法中為所有bean準備了一個后置處理器ApplicationListenerDetector,來看看它的postProcessAfterInitialization方法的代碼,也就是bean在實例化之后要做的事情:
[站外圖片上傳中...(image-c0dd8b-1551080194999)]
核心的一句:
this.applicationContext.addApplicationListener((ApplicationListener<?>) bean); 此代碼注冊監聽器,其實就是保存在成員變量applicationEventMulticaster的成員變量defaultRetriever的集合applicationListeners
即:當前bean實現了ApplicationListener接口,就會調用this.applicationContext.addApplicationListener方法將當前bean注冊到applicationContext的監聽器集合中,后面有廣播就直接找到這些監聽器,調用每個監聽器的onApplicationEvent方法;

自定義的消息監聽器可以指定消息類型,所有的廣播消息中,這個監聽器只會收到自己指定的消息類型的廣播,spring是如何做到這一點的?

4 如何做到只接收指定類型的

自定義監聽器只接收指定類型的消息,以下兩種方案都可以實現:
1.注冊監聽器的時候,將監聽器和消息類型綁定; 2.廣播的時候,按照這條消息的類型去找指定了該類型的監聽器,但不可能每條廣播都去所有監聽器里面找一遍,應該是說廣播的時候會觸發一次監聽器和消息的類型綁定;

spring如何處理?

先看注冊監聽器的代碼

按照之前的分析,注冊監聽發生在后置處理器ApplicationListenerDetector中,看看this.applicationContext.addApplicationListener這一行代碼的內部邏輯:

[站外圖片上傳中...(image-f3b4e2-1551080194999)]
繼續往下debug:


image

把監聽器加入集合defaultRetriever.applicationListeners中,這是個LinkedHashSet實例
this.defaultRetriever.applicationListeners.add(listener);
注冊監聽器,其實就是把ApplicationListener的實現類放入一個LinkedHashSet的集合,此處沒有任何與消息類型相關的操作,因此,監聽器注冊的時候并沒有將消息類型和監聽器綁定

去看廣播消息的代碼
來到SimpleApplicationEventMulticastermulticastEvent方法

image

可以看到方法getApplicationListeners(event, type),包含了listenertype
即,在發送消息的時候根據類型去找所有對應的監聽器;

protected Collection<ApplicationListener<?>> getApplicationListeners(
            ApplicationEvent event, ResolvableType eventType) {

    Object source = event.getSource();

    Class<?> sourceType = (source != null ? source.getClass() : null);
    //緩存的key有兩個維度:消息來源+消息類型(關于消息來源可見ApplicationEvent構造方法的入參)
    ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

    // retrieverCache是ConcurrentHashMap對象,所以是線程安全的,
    // ListenerRetriever中有個監聽器的集合,并有些簡單的邏輯封裝,調用它的getApplicationListeners方法返回的監聽類集合是排好序的(order注解排序)
    ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
    if (retriever != null) {
        //如果retrieverCache中找到對應的監聽器集合,就立即返回了
        return retriever.getApplicationListeners();
    }

    if (this.beanClassLoader == null ||
            (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
                    (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
        //如果retrieverCache中沒有數據,就在此查出數據并放入緩存,
        //先加鎖
        synchronized (this.retrievalMutex) {
            //雙重判斷的第二重,避免自己在BLOCK的時候其他線程已經將數據放入緩存了
            retriever = this.retrieverCache.get(cacheKey);
            if (retriever != null) {
                return retriever.getApplicationListeners();
            }
            //新建一個ListenerRetriever對象
            retriever = new ListenerRetriever(true);
            //retrieveApplicationListeners方法復制找出某個消息類型加來源類型對應的所有監聽器
            Collection<ApplicationListener<?>> listeners =
                    retrieveApplicationListeners(eventType, sourceType, retriever);
            //存入retrieverCache  
            this.retrieverCache.put(cacheKey, retriever);
            //返回結果
            return listeners;
        }
    }
    else {
        // No ListenerRetriever caching -> no synchronization necessary
        return retrieveApplicationListeners(eventType, sourceType, null);
    }
}

在廣播消息的時刻,如果某個類型的消息在緩存中找不到對應的監聽器集合,就調用retrieveApplicationListeners方法去找出符合條件的所有監聽器,然后放入這個集合。跟蹤getApplicationListeners方法,了解如何獲取事件所有的監聽器
getApplicationListeners

image

代碼中可以看到,首先將listener的所有名字生成一個list,從新遍歷這個list,獲取bean對象,生成bean的一個list,然后調用AnnotationAwareOrderComparator.sort(allListeners);對list中的bean進行排序
AnnotationAwareOrderComparatorOrderComparator的子類,用來支持Spring的Ordered類、@Order注解和@Priority注解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容