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注解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容