2.1spring事件之ApplicationListener

(一)理論通識:

我們知道spring事件是基于事件/監聽器編程模型,在這個模型中,有幾個重要的角色,事件(ApplicationEvent),應用事件監聽器(ApplicationListener),以及事件發布者(ApplicationContext)。

(二)需求描述

用戶注冊成功之后,發送短信告知用戶。

(三)代碼實現

我們先看下目錄結構
image.png

先看MyRegisApplicationEvent類,他是繼承ApplicationEvent類:

package com.wangming.event;

import org.springframework.context.ApplicationEvent;

public class MyRegisApplicationEvent extends ApplicationEvent {

   public MyRegisApplicationEvent(Object source) {
       super(source);
   }
}

MyRegisApplicationListener類是實現ApplicationListener接口,注意泛型為MyRegisApplicationEvent類。其中@Order注解表示這個監聽器在容器中的順序,比如我們可以注冊很多監聽,有時候業務需要先執行某個后執行某個,這樣就需要用到排序了:

package com.wangming.listener;

import com.wangming.event.MyRegisApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;

@Order(Ordered.LOWEST_PRECEDENCE+1)
public class MyRegisApplicationListener implements ApplicationListener<MyRegisApplicationEvent> {
   
    @Override
    public void onApplicationEvent(MyRegisApplicationEvent event) {
        System.out.println("MyRegisApplicationListener:name:"+event.getClass().getName());
        System.out.println("MyRegisApplicationListener:getSource:"+event.getSource());
        System.out.println("發送短信告知用戶注冊成功!");

    }
}

ApplicationListenerBootstrap引導啟動類:

package com.wangming.bootstrap;

import com.wangming.listener.MyRegisApplicationListener;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(scanBasePackages = "com.wangming")
@EnableScheduling
public class ApplicationListenerBootstrap {

    public static void main(String[] args) {
        ConfigurableApplicationContext run =
                new SpringApplicationBuilder(ApplicationListenerBootstrap.class)
//                        .web(WebApplicationType.NONE)
                        .run(args);
  //此處我們可以使用之前的spring工廠加載機制,在resource目錄下建立resources/META-INF/spring.factories文件,并在文件中如此配置
//# ApplicationListener 實現配置
//#org.springframework.context.ApplicationListener=\
//#com.wangming.listener.MyRegisApplicationListenertener

         run.addApplicationListener(new MyRegisApplicationListener());

    }

}

最后寫一個注冊接口RegisController,通過裝配的ApplicationContext 進行事件發布:

package com.wangming.controller;

import com.wangming.event.MyRegisApplicationEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RegisController {

    @Autowired
    ApplicationContext context;

    @GetMapping("/hello")
    public String regis() {
        //....
        context.publishEvent(new MyRegisApplicationEvent("注冊成功"));
        return "ok";
    }
}

讓我們調用一下注冊接口,看看控制臺:
image.png

發現確實如我們預期那樣。

(四)源碼解讀

我們以context.publishEvent(new MyRegisApplicationEvent("注冊成功"));這句代碼為契機點,往下走:
AbstractApplicationContext類中關鍵代碼:

...
/**
     * Publish the given event to all listeners.
     * @param event the event to publish (may be an {@link ApplicationEvent}
     * or a payload object to be turned into a {@link PayloadApplicationEvent})
     * @param eventType the resolved event type, if known
     * @since 4.2
     */
    protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
        Assert.notNull(event, "Event must not be null");
        if (logger.isTraceEnabled()) {
            logger.trace("Publishing event in " + getDisplayName() + ": " + event);
        }

        // Decorate event as an ApplicationEvent if necessary
        ApplicationEvent applicationEvent;
        if (event instanceof ApplicationEvent) {
            applicationEvent = (ApplicationEvent) event;
        }
        else {
            applicationEvent = new PayloadApplicationEvent<>(this, event);
            if (eventType == null) {
                eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType();
            }
        }

        // Multicast right now if possible - or lazily once the multicaster is initialized
        if (this.earlyApplicationEvents != null) {
            this.earlyApplicationEvents.add(applicationEvent);
        }
        else {
            getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
        }

        // Publish event via parent context as well...
        if (this.parent != null) {
            if (this.parent instanceof AbstractApplicationContext) {
                ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
            }
            else {
                this.parent.publishEvent(event);
            }
        }
    }
...

其中這行代碼 getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);非常值得我們關注,ApplicationEventMulticaster這個類是我們所說的廣播器,在spring事件中起到了非常關鍵的作用,發布者發布的事件會發送給EventMultiCaster, 而后由EventMultiCaster注冊著所有的Listener,然后根據事件類型決定轉發給那個Listener。
Spring 應用事廣播器的默認實現類為SimpleApplicationEventMulticaster,我們繼續往下走就會進入SimpleApplicationEventMulticaster類中,然我們瞅瞅關鍵代碼:

...
@Override
   public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
       ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
       for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
           Executor executor = getTaskExecutor();
           if (executor != null) {
               executor.execute(() -> invokeListener(listener, event));
           }
           else {
               invokeListener(listener, event);
           }
       }
   }
...
/**
    * Invoke the given listener with the given event.
    * @param listener the ApplicationListener to invoke
    * @param event the current event to propagate
    * @since 4.1
    */
   protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
       ErrorHandler errorHandler = getErrorHandler();
       if (errorHandler != null) {
           try {
               doInvokeListener(listener, event);
           }
           catch (Throwable err) {
               errorHandler.handleError(err);
           }
       }
       else {
           doInvokeListener(listener, event);
       }
   }
...
@SuppressWarnings({"unchecked", "rawtypes"})
   private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
       try {
           listener.onApplicationEvent(event);
       }
       catch (ClassCastException ex) {
           String msg = ex.getMessage();
           if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
               // Possibly a lambda-defined listener which we could not resolve the generic event type for
               // -> let's suppress the exception and just log a debug message.
               Log logger = LogFactory.getLog(getClass());
               if (logger.isDebugEnabled()) {
                   logger.debug("Non-matching event type for listener: " + listener, ex);
               }
           }
           else {
               throw ex;
           }
       }
   }
...

最終我們看到了真正回調的地方 listener.onApplicationEvent(event);。
這里說一個題外話,我們在SimpleApplicationEventMulticaster廣播器類中發現這樣一段代碼:

    Executor executor = getTaskExecutor();
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }

說明我們監聽器中的業務代碼是可以異步執行的。我們怎么實現呢?很簡單,做三處修改,1在引導啟動類上加上@EnableAsync,2 注釋掉這行代碼run.addApplicationListener(new MyRegisApplicationListener());

package com.wangming.bootstrap;

import com.wangming.listener.MyRegisApplicationListener;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(scanBasePackages = "com.wangming")
@EnableScheduling
@EnableAsync
public class ApplicationListenerBootstrap {

    public static void main(String[] args) {
        ConfigurableApplicationContext run =
                new SpringApplicationBuilder(ApplicationListenerBootstrap.class)
//                        .web(WebApplicationType.NONE)
                        .run(args);
 
//        run.addApplicationListener(new MyRegisApplicationListener());
 
    }

}

第三就是在MyRegisApplicationListener監聽類上打上
@Component
@Async這兩個注解
剛剛之所以屏蔽掉run.addApplicationListener(new MyRegisApplicationListener());因為@Component注解已經幫我們注入了,如果不屏蔽會有兩次回調。

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

推薦閱讀更多精彩內容

  • 路過一個陜西特色小吃店,位于星海廣場薄太后涼皮店。裝修風格很優雅,很溫馨,忍不住進去點餐嘗嘗。 吃了他們家的涼皮和...
    肖家菇涼閱讀 211評論 0 0
  • 【 兵法:一曰度,二曰量,三曰數,四曰稱,五曰勝。地生度,度生量,量生數,數生稱,稱生勝。故勝兵若以鎰稱銖,敗兵若...
    靖宇海波閱讀 573評論 0 1
  • 其實很多事明明都知道結果,還是要等著看。終于有了結果,你的路確實很不好走。兜兜轉轉總是在途中,又回到混沌初始的樣子...
    mimonimo閱讀 215評論 0 1
  • 現在很多家長每天都非常忙,一大早去上班,很晚才回家,甚至和孩子同住一起,孩子也只是周末才看到父母。 也有種情況就是...
    何以安歌閱讀 661評論 0 0