【啃啊啃 Spring5 源碼】細(xì)碎一:spring 事件機(jī)制

閱讀spring源碼時(shí),看到ApplicationEvent相關(guān)的代碼覺得熟悉又困惑忙上,深入了解了一下拷呆,發(fā)現(xiàn)原來是spring事件機(jī)制(原諒我之前沒用過……)。
這里在【Spring4揭秘 基礎(chǔ)1】監(jiān)聽器和事件的基礎(chǔ)下進(jìn)行一下擴(kuò)展深入疫粥,感謝這篇博文的作者茬斧,他的spring基礎(chǔ)系列文章讓我在閱讀源碼時(shí),輕松了不少梗逮。

注:源碼部分根據(jù)spring-5.0.7版本分析

設(shè)計(jì)模式

spring事件機(jī)制其實(shí)就是觀察者模式的一種體現(xiàn)项秉。忘記或不熟悉觀察者模式的朋友可以看我前面的總結(jié):Head First 設(shè)計(jì)模式(二)觀察者模式

觀察者模式簡(jiǎn)單可分為兩部分:主題觀察者。當(dāng)一個(gè)主題改變狀態(tài)時(shí)慷彤,它的所有依賴者都會(huì)收到通知并進(jìn)行自動(dòng)更新娄蔼。

Spring事件機(jī)制簡(jiǎn)單可分為三部分:事件廣播底哗、觀察者岁诉。 “主題改變狀態(tài)” 這個(gè)動(dòng)作被抽離成了 一個(gè)“事件”,由一個(gè)持有所有觀察者的“廣播容器” 進(jìn)行廣播跋选,“觀察者”們 接收到相應(yīng)事件后進(jìn)行自動(dòng)更新涕癣。

這種設(shè)計(jì)其實(shí)繼承自Java本身的事件機(jī)制:

  1. java.util.EventObject
    事件狀態(tài)對(duì)象的基類,它封裝了事件源對(duì)象以及和事件相關(guān)的信息前标。所有java的事件類都需要繼承該類坠韩。
  2. java.util.EventListener
    觀察者基類,當(dāng)事件源的屬性或狀態(tài)改變的時(shí)候炼列,調(diào)用相應(yīng)觀察者內(nèi)的回調(diào)方法只搁。
  3. Source
    主題類,java中未定義俭尖,持有所有的觀察者氢惋,當(dāng)主題狀態(tài)發(fā)生改變,產(chǎn)生事件,負(fù)責(zé)向所有觀察者發(fā)布事件

Java的事件機(jī)制這里不敞開講明肮,想了解可看:java 事件機(jī)制

Spring中的事件機(jī)制

Spring的事件機(jī)制相關(guān)的核心類有四個(gè):

  • ApplicationEvent: Spring中的事件基類菱农,繼承自java.util.EventObject,創(chuàng)建是需要指定事件源
public abstract class ApplicationEvent extends EventObject {
    /**
     * 創(chuàng)建一個(gè)事件柿估,需要指定事件源
     */
    public ApplicationEvent(Object source) {
        super(source);
        this.timestamp = System.currentTimeMillis();
    }
}
  • ApplicationEventPublisher:發(fā)布事件者循未,調(diào)用廣播發(fā)布事件
public interface ApplicationEventPublisher {
    /**發(fā)布事件*/
    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }
    void publishEvent(Object event);
}
  • ApplicationEventMulticaster:廣播,持有觀察者集合秫舌,可向集合內(nèi)的所有觀察者通知事件
public interface ApplicationEventMulticaster {
    /**
     * 添加監(jiān)聽者(觀察者)
     */
    void addApplicationListener(ApplicationListener<?> listener);

    /**
     * 刪除監(jiān)聽者(觀察者)
     */
    void removeApplicationListener(ApplicationListener<?> listener);

    /**
     * 向所有監(jiān)聽者發(fā)布事件
     */
    void multicastEvent(ApplicationEvent event);
}
  • ApplicationListener:觀察者的妖,接收對(duì)應(yīng)事件后,執(zhí)行邏輯
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    /**
     * 接收事件后足陨,執(zhí)行相應(yīng)邏輯
     */
    void onApplicationEvent(E event);
}

事件發(fā)布者ApplicationEventPublisher持有廣播嫂粟,而廣播ApplicationEventMulticaster持有若干觀察者ApplicationListener。一個(gè)事件ApplicationEvent可以通過發(fā)布者ApplicationEventPublisher發(fā)布后墨缘,會(huì)調(diào)用廣播ApplicationEventMulticaster通知所有觀察者星虹,觀察者ApplicationListener收到通知后執(zhí)行相關(guān)操作。

下面舉例說明:
當(dāng)一個(gè)用戶注冊(cè)結(jié)束后镊讼,我們想要將這個(gè)事件發(fā)生給短信監(jiān)聽者和郵件監(jiān)聽者宽涌,讓他們向用戶發(fā)送短信和郵件。

public class EventDemo {

    public static void main(String[] args) {
        //構(gòu)建廣播器
        ApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        //廣播添加監(jiān)聽器
        multicaster.addApplicationListener(new RegisterListener1());
        multicaster.addApplicationListener(new RegisterListener2());

        //構(gòu)建事件發(fā)布者
        MyEventPublicsher eventPublicsher = new MyEventPublicsher();
        //事件發(fā)布者增加廣播
        eventPublicsher.setEventMulticaster(multicaster);

        //構(gòu)建注冊(cè)事件
        User user = new User("jack", "18782252509", "jack_email@163.com");
        System.out.println("用戶注冊(cè)……");
        RegisterEvent registerEvent = new RegisterEvent(user);

        //發(fā)布注冊(cè)事件
        eventPublicsher.publishEvent(registerEvent);
    }

    /**
     * 用戶實(shí)體類
     */
    public static class User{
        private String id;
        private String name;
        private String phone;
        private String email;

        public User(String name, String phone, String email) {
            this.name = name;
            this.phone = phone;
            this.email = email;
        }

        //.....GET AND SET

    }
    /**
     * 自定義注冊(cè)事件
     */
    public static class RegisterEvent extends ApplicationEvent {
        //事件的構(gòu)造方法中蝶棋,必須制定事件源
        public RegisterEvent(User user) {
            super(user);
        }

        public User getUser(){
            return (User) getSource();
        }
    }

    /**
     * 注冊(cè)事件監(jiān)聽者1-短信監(jiān)聽者(即觀察者),負(fù)責(zé)注冊(cè)后發(fā)生短信
     * 注意:實(shí)現(xiàn)接口時(shí)卸亮,在泛形中指定事件類型,則只監(jiān)聽該類型事件玩裙。若不指定兼贸,則默認(rèn)監(jiān)聽所有事件。
     */
    public static class RegisterListener1 implements ApplicationListener<RegisterEvent> {
        public void onApplicationEvent(RegisterEvent event) {
            User user = event.getUser();
            System.out.println("用戶:"+ user.getName()+"注冊(cè)結(jié)束吃溅,向手機(jī)"+user.getPhone()+"發(fā)送短信!");
        }
    }

    /**
     * 注冊(cè)事件監(jiān)聽者2-郵件監(jiān)聽者(即觀察者)溶诞,負(fù)責(zé)注冊(cè)后發(fā)送郵件
     * 注意:實(shí)現(xiàn)接口時(shí),在泛形中指定事件類型罕偎,則只監(jiān)聽該類型事件很澄。若不指定,則默認(rèn)監(jiān)聽所有事件颜及。
     */
    public static class RegisterListener2 implements ApplicationListener<RegisterEvent> {

        public void onApplicationEvent(RegisterEvent event) {
            User user = event.getUser();
            System.out.println("用戶:"+ user.getName()+"注冊(cè)結(jié)束,發(fā)生郵件:"+user.getEmail());
        }
    }


    /**
     * 事件發(fā)布者蹂楣,持有監(jiān)聽者
     */
    public static class MyEventPublicsher implements  ApplicationEventPublisher{
        //廣播
        private ApplicationEventMulticaster eventMulticaster;

        public void setEventMulticaster(ApplicationEventMulticaster eventMulticaster) {
            this.eventMulticaster = eventMulticaster;
        }

        //發(fā)布事件
        public void publishEvent(Object event) {
            eventMulticaster.multicastEvent((ApplicationEvent) event);
        }
    }

}

輸出:

用戶注冊(cè)后
用戶:jack注冊(cè)結(jié)束俏站,向手機(jī)18782252509發(fā)送短信!
用戶:jack注冊(cè)結(jié)束,發(fā)生郵件:jack_email@163.com

源碼細(xì)節(jié)解析

我們主要分析下廣播的細(xì)節(jié)痊土,以SimpleApplicationEventMulticaster為例:

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
        @Override
        public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
            //事件被統(tǒng)一封裝成了ResolvableType肄扎,方便形參入口統(tǒng)一
            ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
            //根據(jù)事件類型,通過泛形反射獲取對(duì)應(yīng)的監(jiān)聽者
            for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
                //獲取廣播配置的線程池
                Executor executor = getTaskExecutor();
                //如果有配置線程池,則異步通知事件監(jiān)聽者
                if (executor != null) {
                    executor.execute(() -> invokeListener(listener, event));
                }
                //沒有配置線程池犯祠,同步通知事件監(jiān)聽者
                else {
                    invokeListener(listener, event);
                }
            }
        }
        
        private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
                try {
                    //執(zhí)行監(jiān)聽者對(duì)應(yīng)邏輯
                    listener.onApplicationEvent(event);
                }
                catch (ClassCastException ex) {
                    ……
                }
            }
}

我們可以看到spring廣播時(shí)旭等,會(huì)先去判斷有沒有配置線程池,如果配置則使用線程池異步執(zhí)行監(jiān)聽者邏輯衡载,否則同步搔耕。

需要注意的是,我們使用spring事件機(jī)制時(shí)痰娱,默認(rèn)是沒有配置線程池的弃榨,也就是默認(rèn)所有的通知都是同步的,需要手動(dòng)指定線程池才會(huì)開啟同步梨睁。

應(yīng)用

設(shè)計(jì)一個(gè)業(yè)務(wù)場(chǎng)景:當(dāng)一個(gè)用戶完成貸款訂單后鲸睛,我們希望執(zhí)行發(fā)送提醒短信、調(diào)用積分服務(wù)增加積分坡贺、通知風(fēng)控服務(wù)重算風(fēng)控值(后續(xù)操作可能增加)等功能官辈。這種業(yè)務(wù)需求開始很可能寫成同步代碼。

//創(chuàng)建訂單
public void createOrder(Order order){
    創(chuàng)建貸款訂單遍坟;
    發(fā)送提醒短信钧萍;
    調(diào)用積分服務(wù)增加積分;
    調(diào)用風(fēng)控服務(wù)推送訂單信息政鼠;
    ……
    返回风瘦;
}

隨著業(yè)務(wù)復(fù)雜度的增加,我們很快發(fā)現(xiàn)createOrder()這個(gè)方法耦合了太多與創(chuàng)建訂單無(wú)關(guān)的邏輯公般,即影響了原本創(chuàng)建訂單方法的效率万搔,在設(shè)計(jì)上又不符合“開閉原則”。

現(xiàn)在使用spring事件機(jī)制我們來解耦官帘,將與注冊(cè)無(wú)關(guān)的操作改為異步瞬雹。這里直接使用注解式寫法。

  • 首先我們修改spring中的廣播刽虹,為它注入我們自定義的線程池酗捌,在spring配置加上:
    <!--自定義線程池-->
    <bean id="myExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />

    <!--修改容器中的廣播,注入自定義線程池-->
    <bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
        <property name="taskExecutor" ref="myExecutor" />
    </bean>
  • 定義一個(gè)創(chuàng)建訂單事件
/**
 * 創(chuàng)建訂單完成事件
 */
@Component
public class AfterCreateOrderEvent extends ApplicationEvent {

    public AfterCreateOrderEvent(Order order) {
        super(order);
    }

    public Order getOrder(){
        return (Order) getSource();
    }
}
  • 使用事件機(jī)制改變?cè)写a
@Service
public class OrderService {
    //直接注入spring事件發(fā)布者
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
    
    /**
     * 簡(jiǎn)單的創(chuàng)建訂單方法
     */
    public void createOrder(Order order) throws InterruptedException {
        System.out.println("創(chuàng)建訂單 order:"+order.getOrderNo()+" 結(jié)束");
        //調(diào)用事件發(fā)布者發(fā)布事件
        applicationEventPublisher.publishEvent(new AfterCreateOrderEvent(order));
        System.out.println("createOrder方法 結(jié)束");
    }

     //加入@EventListener注解后涌哲,該方法可以看出一個(gè)事件監(jiān)聽者
    @EventListener
    public void afterCreateOrder(AfterCreateOrderEvent afterCreateOrderEvent) throws InterruptedException {
        Order order = afterCreateOrderEvent.getOrder();
        Thread.sleep(2000);
        System.out.println("調(diào)用短信通知服務(wù):" + order.getPhone());
        System.out.println("調(diào)用積分服務(wù)增加貸款積分:"+order.getOrderNo());
    }

    public static void main(String[] args) throws InterruptedException {
        Order order = new Order("N123124124124", "18782202534");
        //這里指定自己的spring配置路徑
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("config/spring-config.xml");

        OrderService orderService = context.getBean(OrderService.class);
        orderService.createOrder(order);
    }
}

輸出:

創(chuàng)建訂單 order:N123124124124 結(jié)束
createOrder方法 結(jié)束
調(diào)用短信通知服務(wù):18782202534
調(diào)用積分服務(wù)增加貸款積分:N123124124124

自此胖缤,創(chuàng)建訂單與其他操作便實(shí)現(xiàn)了異步和解耦。

另一種異步實(shí)現(xiàn)方式

另外阀圾,也可使用@Async注解來實(shí)現(xiàn)事件的異步調(diào)用

    @EventListener
    @Async
    public void afterCreateOrder(AfterCreateOrderEvent afterCreateOrderEvent) throws InterruptedException {
        Order order = afterCreateOrderEvent.getOrder();
        Thread.sleep(2000);
        System.out.println("調(diào)用短信通知服務(wù):" + order.getPhone());
        System.out.println("調(diào)用積分服務(wù)增加貸款積分:"+order.getOrderNo());
    }

spring配置加上:

    <!--開啟異步調(diào)用哪廓,并指定線程池--> 
    <task:annotation-driven executor="annotationExecutor" />
    <!--線程池-->
    <task:executor id="annotationExecutor" pool-size="20"/>

但這種方法有弊端,afterCreateOrder()方法不能放在同一類(OrderService)里面初烘。原因是spring的代理機(jī)制涡真。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末分俯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子哆料,更是在濱河造成了極大的恐慌缸剪,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件东亦,死亡現(xiàn)場(chǎng)離奇詭異杏节,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)讥此,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門拢锹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人萄喳,你說我怎么就攤上這事卒稳。” “怎么了他巨?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵充坑,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我染突,道長(zhǎng)捻爷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任份企,我火速辦了婚禮也榄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘司志。我一直安慰自己甜紫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布骂远。 她就那樣靜靜地躺著囚霸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪激才。 梳的紋絲不亂的頭發(fā)上拓型,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音瘸恼,去河邊找鬼劣挫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛钞脂,可吹牛的內(nèi)容都是我干的揣云。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼冰啃,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼邓夕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阎毅,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤焚刚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后扇调,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矿咕,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年狼钮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了碳柱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡熬芜,死狀恐怖莲镣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涎拉,我是刑警寧澤瑞侮,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站鼓拧,受9級(jí)特大地震影響半火,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜季俩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一钮糖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酌住,春花似錦店归、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至祭示,卻和暖如春肄满,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背质涛。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工稠歉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人汇陆。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓怒炸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親毡代。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阅羹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理勺疼,服務(wù)發(fā)現(xiàn),斷路器捏鱼,智...
    卡卡羅2017閱讀 134,657評(píng)論 18 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架执庐,建立于...
    Hsinwong閱讀 22,403評(píng)論 1 92
  • 前言 在微服務(wù)架構(gòu)的系統(tǒng)中,我們通常會(huì)使用輕量級(jí)的消息代理來構(gòu)建一個(gè)共用的消息主題讓系統(tǒng)中所有微服務(wù)實(shí)例都連接上來...
    Chandler_玨瑜閱讀 6,576評(píng)論 2 39
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,811評(píng)論 6 342
  • (上一章) 丨全文總目錄丨 神秘消失的情人导梆。 納西族世代流傳的詭異傳說轨淌。 可怖的黑衣引渡者。 徘徊在麗江的死亡詛咒...
    小巫先生閱讀 411評(píng)論 1 6