Spring Events

1. 概述

事件是框架中最容易被忽視的功能之一,但同時(shí)也是一個(gè)很有用的功能。像Spring其他特性一樣卦睹,事件發(fā)布是ApplicationContext提供的功能之一金句。

事件通知是一個(gè)很有用的功能逗物,使用事件機(jī)制可以將互相耦合的代碼進(jìn)行解耦搬卒,方便功能的新增或修改。

2. 自定義事件

Spring允許創(chuàng)建和發(fā)布自定義事件翎卓,默認(rèn)情況下契邀,事件都是同步執(zhí)行的。這樣有很多好處失暴,比如事件的監(jiān)聽(tīng)器和發(fā)布者在同一個(gè)事務(wù)內(nèi)坯门,能夠很方便的處理一些業(yè)務(wù)。

2.1. 一個(gè)簡(jiǎn)單的Application Event

創(chuàng)建一個(gè)簡(jiǎn)單的事件類(lèi)逗扒,使用一個(gè)String變量來(lái)存儲(chǔ)事件數(shù)據(jù)古戴。

public class CustomSpringEvent extends ApplicationEvent {

    @Getter
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

}

2.2. 事件發(fā)布者

現(xiàn)在來(lái)創(chuàng)建事件發(fā)布者。發(fā)布者創(chuàng)建事件對(duì)象矩肩,并把事件發(fā)送給所有的監(jiān)聽(tīng)器现恼。

要發(fā)布事件,發(fā)布者可以簡(jiǎn)單的注入ApplicationEventPublisher然后使用它的publishEvent()方法:

@Slf4j
@Component
public class CustomSpringEventPublisher {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void doStuffAndPublishAnEvent(final String message) {
        log.info("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }

}

或者黍檩,發(fā)布者也可以實(shí)現(xiàn)ApplicationEventPublisherAware接口述暂。通常情況下使用@Autowired注入會(huì)更簡(jiǎn)單。

2.3. 事件監(jiān)聽(tīng)器

最后建炫,來(lái)創(chuàng)建事件監(jiān)聽(tīng)器。

監(jiān)聽(tīng)器的唯一要求是一個(gè)bean并實(shí)現(xiàn)ApplicationListener接口:

@Slf4j
@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {

    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        log.info("Received spring custom event - {}", event.getMessage());
    }
    
}

上面已經(jīng)說(shuō)過(guò)疼蛾,默認(rèn)情況下事件都是同步執(zhí)行的肛跌,在所有的監(jiān)聽(tīng)器完成對(duì)事件的處理之前,doStuffAndPublishAnEvent() 方法會(huì)一直堵塞察郁。

如果需要指定監(jiān)聽(tīng)器的執(zhí)行順序衍慎,可以實(shí)現(xiàn)Ordered接口設(shè)置每個(gè)執(zhí)行器的優(yōu)先級(jí)。

3. 創(chuàng)建異步事件

在某些情況下皮钠,同步處理事件并不是我們想要的效果稳捆,我們可能需要異步處理事件。

AbstractApplicationContext中存在一個(gè)ApplicationEventMulticaster對(duì)事件進(jìn)行廣播麦轰,默認(rèn)情況下框架初始化了一個(gè)SimpleApplicationEventMulticaster

/**
* Initialize the ApplicationEventMulticaster.
* Uses SimpleApplicationEventMulticaster if none defined in the context.
* @see org.springframework.context.event.SimpleApplicationEventMulticaster
*/
protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    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是如何進(jìn)行事件廣播的呢乔夯?

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

如果存在Executor,那么事件就會(huì)進(jìn)行異步處理款侵,否則就是同步末荐。

所以需要異步的處理事件,那么就需要手動(dòng)創(chuàng)建一個(gè)name為applicationEventMulticaster的bean新锈,然后為它設(shè)置一個(gè)TaskExecutor甲脏,例如:

@Configuration
public class AsynchronousSpringEventsConfig {

    @Bean(name = AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }

}

事件、發(fā)布者和監(jiān)聽(tīng)器和前面定義相同,這樣監(jiān)聽(tīng)器就會(huì)在單獨(dú)的線程中異步處理事件块请。

但是娜氏,這樣配置的話,所有發(fā)布的事件都會(huì)以異步的方式進(jìn)行處理墩新,顯然太簡(jiǎn)單粗暴了贸弥,下面介紹一種更加友好的方式。

首先抖棘,刪除剛剛的AsynchronousSpringEventsConfig配置類(lèi)茂腥,然后在Application主類(lèi)上加上@EnableAsync注解,來(lái)讓程序支持異步方法的調(diào)用切省,最后在監(jiān)聽(tīng)器的onApplicationEvent方法上加上@Async注解最岗。

@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

@Slf4j
@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {

    @Async
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        log.info("Received spring custom event - {}", event.getMessage());
    }

}

4. 基于注解的事件監(jiān)聽(tīng)器

從Spring4.2開(kāi)始,事件監(jiān)聽(tīng)器不再需要實(shí)現(xiàn)ApplicationListener 接口朝捆,可以通過(guò)@EventListener注解在一個(gè)bean的任意public方法上注冊(cè):

@Slf4j
@Component
public class AnnotationDrivenContextStartedListener {

    @EventListener
    public void handleContextStartedEvent(ContextStartedEvent event) {
        log.info("Handling context started event.");
    }

}

如果方法需要監(jiān)聽(tīng)多個(gè)事件或者你不想在方法上定義參數(shù)般渡,那么你可以在注解里設(shè)置事件類(lèi)型,例如:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

注解的condition屬性可以定義一個(gè)SpEL表達(dá)式來(lái)對(duì)事件進(jìn)行過(guò)濾芙盘,表達(dá)式匹配才能調(diào)用特定事件的監(jiān)聽(tīng)方法驯用。

@EventListener(condition = "event.message == 'message'")
public void handleCustomSpringEvent(CustomSpringEvent event) {
    log.info("Received spring custom event - {}", event.getMessage());
}

如果處理完成一個(gè)事件后需要發(fā)布一個(gè)事件,那么你可以在方法上返回相應(yīng)的事件儒老,例如:

@EventListener
public ReturnEvent handleCustomSpringEvent(CustomSpringEvent event) {
    // 首先處理CustomSpringEvent事件
    // 處理完成后蝴乔,發(fā)布ReturnEvent事件
}

如果需要返回多個(gè)事件,那么可以返回事件的集合驮樊。

4.1 異步監(jiān)聽(tīng)器

監(jiān)聽(tīng)器的異步處理可以通過(guò)@Async注解來(lái)實(shí)現(xiàn)薇正。

@Async
@EventListener
public void handleCustomSpringEvent(CustomSpringEvent event) {
    // CustomSpringEvent會(huì)在一個(gè)獨(dú)立的線程中進(jìn)行處理
}

當(dāng)使用異步事件監(jiān)聽(tīng)時(shí),需要注意以下限制:

  • 如果異步事件監(jiān)聽(tīng)器拋出Exception囚衔,不會(huì)將其傳播到調(diào)用方挖腰。可以查看AsyncUncaughtExceptionHandler來(lái)獲取更多詳細(xì)信息练湿。
  • 異步事件監(jiān)聽(tīng)方法無(wú)法通過(guò)返回值來(lái)發(fā)布后續(xù)事件猴仑。如果你確實(shí)需要發(fā)布后續(xù)事件,可以注入ApplicationEventPublisher來(lái)手動(dòng)發(fā)布肥哎。

4.2 排序監(jiān)聽(tīng)器

如果需要指定監(jiān)聽(tīng)器的執(zhí)行順序辽俗,可以在方法上使用@Order注解。

@Order(18)
@EventListener
public void handleCustomSpringEvent(CustomSpringEvent event) {
    // ...
}

5. 泛型事件

不是所有的事件都必須繼承ApplicationEvent 篡诽,ApplicationEventPublisher中有2種方式進(jìn)行事件的發(fā)布榆苞。

@FunctionalInterface
public interface ApplicationEventPublisher {

    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }

    void publishEvent(Object event);

}

通過(guò)查看實(shí)現(xiàn)類(lèi)代碼可以發(fā)現(xiàn),發(fā)送的Object類(lèi)型的事件最后會(huì)被包裝為一個(gè)PayloadApplicationEvent霞捡,而PayloadApplicationEvent繼承了ApplicationEvent 坐漏。

// 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();
    }
}

所以我們可以發(fā)布任何Object事件。

@Data
public class ObjectEvent {

    private String message;

    public ObjectEvent(String message) {
        this.message = message;
    }

}

// 發(fā)送事件
applicationEventPublisher.publishEvent(new ObjectEvent(message));

// 監(jiān)聽(tīng)事件
@EventListener
public void handleObjectEvent(ObjectEvent event){
    log.info(event.getMessage());
}

6. 事務(wù)綁定事件

很多時(shí)候,只有事務(wù)提交之后我們才會(huì)發(fā)布相應(yīng)的事件處理其他邏輯赊琳,比如用戶注冊(cè)之后街夭,發(fā)送郵件或者短信。從Spring 4.2開(kāi)始躏筏,框架提供了一個(gè)很方便的注解來(lái)實(shí)現(xiàn)此功能(4.2之前也可以通過(guò)自己寫(xiě)代碼實(shí)現(xiàn))板丽,即@TransactionalEventListener

@TransactionalEventListener是對(duì)@EventListener的一個(gè)擴(kuò)展趁尼,允許將事件的監(jiān)聽(tīng)器綁定到事務(wù)的某個(gè)階段埃碱。可以綁定到以下事務(wù)階段:

  • AFTER_COMMIT (默認(rèn))酥泞,事務(wù)提交后
  • AFTER_ROLLBACK 砚殿,事務(wù)回滾后
  • AFTER_COMPLETION ,事務(wù)完成芝囤,包括提交后和回滾后
  • BEFORE_COMMIT 似炎,事務(wù)提交前
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleTransactionalEvent(TransactionalEvent event) {
    log.info("Handling event inside a transaction BEFORE COMMIT.");
}

只有當(dāng)上下文存在事務(wù),并且事務(wù)提交前悯姊,才會(huì)調(diào)用此監(jiān)聽(tīng)器的方法羡藐。

默認(rèn)情況下,如果上下文不存在事務(wù)悯许,則根本不會(huì)發(fā)送事件仆嗦,我們可以通過(guò)設(shè)置@TransactionalEventListenerfallbackExecution為true來(lái)實(shí)現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末先壕,一起剝皮案震驚了整個(gè)濱河市瘩扼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌启上,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件店印,死亡現(xiàn)場(chǎng)離奇詭異冈在,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)按摘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)包券,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人炫贤,你說(shuō)我怎么就攤上這事溅固。” “怎么了兰珍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵侍郭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)亮元,這世上最難降的妖魔是什么猛计? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮爆捞,結(jié)果婚禮上奉瘤,老公的妹妹穿的比我還像新娘。我一直安慰自己煮甥,他們只是感情好盗温,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著成肘,像睡著了一般卖局。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艇劫,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天吼驶,我揣著相機(jī)與錄音,去河邊找鬼店煞。 笑死蟹演,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的顷蟀。 我是一名探鬼主播酒请,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鸣个!你這毒婦竟也來(lái)了羞反?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤囤萤,失蹤者是張志新(化名)和其女友劉穎昼窗,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體涛舍,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡澄惊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了富雅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掸驱。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖没佑,靈堂內(nèi)的尸體忽然破棺而出毕贼,到底是詐尸還是另有隱情,我是刑警寧澤蛤奢,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布鬼癣,位于F島的核電站陶贼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扣溺。R本人自食惡果不足惜骇窍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锥余。 院中可真熱鬧腹纳,春花似錦、人聲如沸驱犹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)雄驹。三九已至佃牛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間医舆,已是汗流浹背俘侠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔬将,地道東北人爷速。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像霞怀,于是被迫代替她去往敵國(guó)和親惫东。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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

  • 1. 什么是事件監(jiān)聽(tīng)機(jī)制 在講解事件監(jiān)聽(tīng)機(jī)制前,我們先回顧下設(shè)計(jì)模式中的觀察者模式,因?yàn)槭录O(jiān)聽(tīng)機(jī)制可以說(shuō)是在典型...
    habit_learning閱讀 4,134評(píng)論 1 13
  • 前面我們講到了Spring在進(jìn)行事務(wù)邏輯織入的時(shí)候毙石,無(wú)論是事務(wù)開(kāi)始廉沮,提交或者回滾,都會(huì)觸發(fā)相應(yīng)的事務(wù)事件徐矩。本文首先...
    AI喬治閱讀 1,734評(píng)論 0 1
  • 事件驅(qū)動(dòng)模型簡(jiǎn)介 事件驅(qū)動(dòng)模型也就是我們常說(shuō)的觀察者滞时,或者發(fā)布-訂閱模型;理解它的幾個(gè)關(guān)鍵點(diǎn): 1.首先是一種對(duì)象...
    algernoon閱讀 1,660評(píng)論 0 4
  • Spring 框架事件收發(fā)功能的使用 (一) 基本使用 1. 概述 Spring 框架時(shí)間收發(fā)功能本系列的文章總共...
    云逸Dean閱讀 799評(píng)論 0 0
  • 上午先生帶著孩子去超市買(mǎi)菜滤灯,我還有演講作業(yè)要練習(xí)就沒(méi)有去坪稽,他們剛走了沒(méi)多久。我聽(tīng)了下今天的晨讀力喷,關(guān)于減肥了和低糖食...
    利萍閱讀 162評(píng)論 0 1