掌握 Spring 之事件處理

頭圖

1 前言

本次我們來(lái)學(xué)習(xí) Spring 的事件處理,源于實(shí)際工作中遇到的項(xiàng)目需求:在一個(gè)支付的下單場(chǎng)景中,當(dāng)用戶真正支付成功,服務(wù)器收到回調(diào)后就需要及時(shí)更新訂單數(shù)據(jù)狀態(tài)來(lái)保證數(shù)據(jù)一致。通常做法就是在回調(diào)方法里直接使用訂單服務(wù)更新數(shù)據(jù), 然而這樣實(shí)現(xiàn)上兩個(gè)模塊出現(xiàn)了緊密耦合,如果訂單更新的操作需要進(jìn)行調(diào)整,那么在支付回調(diào)的代碼塊中也需要被修改。

為了避免這樣情況發(fā)生,我采用了 Spring 事件發(fā)布與訂閱的方式來(lái)實(shí)現(xiàn)接受支付回調(diào),發(fā)布通知更新訂單狀態(tài)的這個(gè)功能,讓訂單服務(wù)更新數(shù)據(jù)的操作只依賴(lài)特定的事件,而不用關(guān)心具體的觸發(fā)對(duì)象,也能達(dá)到代碼復(fù)用的目的。

本文主要內(nèi)容涉及如下:

  • Spring 標(biāo)準(zhǔn)事件的處理
  • Spring 中自定義事件擴(kuò)展實(shí)現(xiàn)
  • Spring Boot 的事件與偵聽(tīng)

示例項(xiàng)目:

環(huán)境支持:

  • JDK 8
  • SpringBoot 2.1.4
  • Maven 3.6.0

2.1 Spring 標(biāo)準(zhǔn)事件處理

Spring 程序啟動(dòng)過(guò)程中會(huì)有不同的事件通知,內(nèi)置標(biāo)準(zhǔn)的事件有 5 種:

事件 說(shuō)明
ContextRefreshedEvent 當(dāng) Spring 容器處于初始化或者刷新階段時(shí)就會(huì)觸發(fā),事實(shí)是ApplicationContext#refresh()方法被調(diào)用時(shí),此時(shí)容器已經(jīng)初始化完畢。
ContextStartedEvent 當(dāng)調(diào)用 ConfigurableApplicationContext接口下的 start() 方法時(shí)觸發(fā),表示 Spring 容器啟動(dòng);通常用于 Spring 容器顯式關(guān)閉后的啟動(dòng)。
ContextStoppedEvent 當(dāng)調(diào)用 ConfigurableApplicationContext 接口下的 stop()方法時(shí)觸發(fā),表示 Spring 容器停止,此時(shí)能通過(guò)其 start()方法重啟容器。
ContextClosedEvent 當(dāng) Spring 容器調(diào)用 ApplicationContext#close() 方法時(shí)觸發(fā),此時(shí) Spring 的 beans 都已經(jīng)被銷(xiāo)毀,并且不會(huì)重新啟動(dòng)和刷新。
RequestHandledEvent 只在 Web 應(yīng)用下存在,當(dāng)接受到 HTTP 請(qǐng)求并處理后就會(huì)觸發(fā),實(shí)際傳遞的默認(rèn)實(shí)現(xiàn)類(lèi) ServletRequestHandledEvent

通常情況下,Spring 程序都會(huì)接收到 ContextRefreshedEvent, ContextClosedEvent 事件的通知。

知道了 Spring 自帶的事件有哪些后,我們就可以針對(duì)一些場(chǎng)景利用事件機(jī)制來(lái)實(shí)現(xiàn)需求,比如說(shuō)在 Spring 啟動(dòng)后初始化資源,加載緩存數(shù)據(jù)到內(nèi)存中等等。代碼實(shí)現(xiàn)也很簡(jiǎn)單,如下:

@Component
public class InitalizeListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ApplicationContext applicationContext = event.getApplicationContext();
        System.out.println("Spring 容器啟動(dòng)  獲取到 Application Context 對(duì)象 " + applicationContext);
        //TODO 初始化資源,加載緩存數(shù)據(jù)到內(nèi)存
    }
}

// 啟動(dòng) Spring 程序后,控制臺(tái)出現(xiàn)如下日志:
// Spring 容器啟動(dòng)  獲取到 Application Context 對(duì)象 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6950ed69, started on Sun May 26 12:19:33 CST 2019

我們可以從 ContextRefreshedEvent 事件中獲取到 ApplicationContext 對(duì)象,從而獲取 Spring 容器中任何已經(jīng)裝載的 Bean 進(jìn)行自定義的操作。

2.1.1 注解驅(qū)動(dòng)的事件偵聽(tīng)

引入 @EventListener

從 Spring 4.2 開(kāi)始,Spring 又提供了更靈活的,注解驅(qū)動(dòng)的事件偵聽(tīng)處理方式。主要使用 @EventListener 注解來(lái)標(biāo)記需要監(jiān)聽(tīng)程序事件的方法,底層由 EventListenerMethodProcessor 對(duì)象將標(biāo)注的方法轉(zhuǎn)為成 ApplicationListener 實(shí)例。

為什么說(shuō)這個(gè)注解方式偵聽(tīng)事件更加靈活呢,我們可以先看下 @EventListener 注解的源碼。

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {

    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String condition() default "";
}

EventListener 注解主要有兩個(gè)屬性:classesconditionclasses 表示所需要偵聽(tīng)的事件類(lèi)型,是個(gè)數(shù)組,所以允許在單個(gè)方法里進(jìn)行多個(gè)不同事件的偵聽(tīng),以此做到復(fù)用的效果;condition 顧名思義就是用來(lái)定義所偵聽(tīng)事件是否處理的前置條件,這里需要注意的是使用 Spring Expression Language (SpEL)定義條件,比如 #root.event 表示了具體的 ApplicationEvent對(duì)象, 使用方式可以參考下方示例代碼:

@Component
public class AnnotationListener {

    @EventListener(value = {ContextRefreshedEvent.class, ContextStartedEvent.class, ContextStoppedEvent.class, ContextClosedEvent.class, RequestHandledEvent.class}, condition = "#root.event != null")
    public void listener(ApplicationEvent event) {
        System.out.println(Thread.currentThread() + " 接收到 Spring 事件:" + event);
    }
}

這里需要注意的是,注解 @EventListener標(biāo)記的方法參數(shù)類(lèi)型不再限制必須是 ApplicationEvent的子類(lèi),沒(méi)有實(shí)現(xiàn) ApplicationListener 接口方法的約束,也讓事件變得更加靈活。

事件的傳遞

另外,使用 @EventListener 還支持事件的傳遞,將當(dāng)前事件處理好的結(jié)果封裝后發(fā)布一個(gè)新的事件,實(shí)現(xiàn)的方式就是讓偵聽(tīng)方法返回非 null 值時(shí),就視為事件繼續(xù)傳播,如下面的示例代碼:

@Component
@Order(2)
public class CustomEventListener {
    @EventListener
    public SecondCustomEvent listener(CustomEvent event) {
        System.out.println(Thread.currentThread() + "CustomEventListener接受到自定義事件:" + event);
        return new SecondCustomEvent(this, event.toString());
    }
}

2.1.2 偵聽(tīng)器優(yōu)先級(jí)

當(dāng)我們對(duì)單個(gè)事件存在多個(gè)偵聽(tīng)器時(shí),可能會(huì)由于需求想要指定偵聽(tīng)器的執(zhí)行順序,這一點(diǎn) Spring 也為我們考慮到了,只要使用 @Order注解聲明監(jiān)聽(tīng)類(lèi)或者監(jiān)聽(tīng)方法即可,根據(jù) @Ordervalue 大小來(lái)確定執(zhí)行順序,越小越優(yōu)先執(zhí)行。

@EventListener
@Order(42)
public void processEvent(Event event) {
}

2.2 自定義事件

在了解如何偵聽(tīng) Spring 事件后,我們?cè)賮?lái)看下如何實(shí)現(xiàn)自定義的事件發(fā)布和偵聽(tīng)處理。首先就要介紹 Spring 中事件機(jī)制的三類(lèi)對(duì)象:

  • Event :所需要觸發(fā)的具體事件對(duì)象,通常擴(kuò)展 ApplicationEvent 實(shí)現(xiàn)。
  • Publisher:觸發(fā)事件發(fā)布的對(duì)象,Spring 提供了 ApplicationEventPublisher 對(duì)象供我們使用,使用它的publishEvent() 方法就可以發(fā)布該事件。
  • Listener:偵聽(tīng)事件發(fā)生的對(duì)象,也就是接受回調(diào)進(jìn)行處理的地方,可以通過(guò) 實(shí)現(xiàn) ApplicationListener接口,或者使用前面提到的 @EventListener注解聲明為事件的偵聽(tīng)器。

接下來(lái)就簡(jiǎn)單看下,一個(gè)自定義事件從聲明到發(fā)布訂閱的代碼示例。

2.2.1 自定義 Application Event

public class CustomEvent extends ApplicationEvent {
    private String data;

    public CustomEvent(Object source, String data) {
        super(source);
        this.data = data;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "CustomEvent{" +
                "data='" + data + '\'' +
                ", source=" + source +
                '}';
    }
}

2.2.2 自定義 Publisher

@Component
public class CustomeEventPublisher implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void publishEvent(String message) {
        System.out.println("開(kāi)始發(fā)布事件 " + message);
        applicationEventPublisher.publishEvent(new CustomEvent(this, message));
    }
}

創(chuàng)建事件發(fā)布者有兩種方式,一種是使用 @Autowire注解,通過(guò) Spring 容器的依賴(lài)注入功能,直接注入 ApplicationEventPublisher對(duì)象,或者實(shí)現(xiàn) ApplicationEventPublisherAware接口,在 Spring 容器啟動(dòng)時(shí)由 Spring 設(shè)置。

2.2.3 自定義 Listener

@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
    @Override
    public void onApplicationEvent(CustomEvent event) {
        System.out.println(Thread.currentThread()+"CustomEventListener接受到自定義事件:" + event);
    }
}

定義事件偵聽(tīng)器時(shí),我們通過(guò)實(shí)現(xiàn) ApplicationListener 接口,指定了事件類(lèi)型,這樣在處理事件時(shí)就不避免了事件類(lèi)型判斷和轉(zhuǎn)換。

關(guān)于事件偵聽(tīng)器還需要注意的一點(diǎn)是:Spring 事件處理默認(rèn)是同步的,這一點(diǎn)在 Spring 官方文檔所有提及,我們先解讀下官方描述:

You can register as many event listeners as you wish, but note that, by default, event listeners receive events synchronously. This means that the publishEvent() method blocks until all listeners have finished processing the event. One advantage of this synchronous and single-threaded approach is that, when a listener receives an event, it operates inside the transaction context of the publisher if a transaction context is available. If another strategy for event publication becomes necessary, See the javadoc for Spring’s ApplicationEventMulticaster interface.

當(dāng)發(fā)布者執(zhí)行了 publishEvent() 方法,默認(rèn)情況下方法所在的當(dāng)前線程就會(huì)阻塞,直到所有該事件相關(guān)的偵聽(tīng)器將事件處理完成。而這樣采用單線程同步方式處理的好處主要是可以保證讓事件處理與發(fā)布者處于同一個(gè)事務(wù)環(huán)境里,如果多個(gè)偵聽(tīng)方法涉及到數(shù)據(jù)庫(kù)操作時(shí)保證了事務(wù)的存在。

2.2.4 異步事件處理

當(dāng)然 Spring 也提供了異步偵聽(tīng)事件的方式,這里主要依賴(lài) ApplicationEventMulticaster接口,可以理解為廣播方式,為了便于使用,Spring 提供一個(gè)簡(jiǎn)易的實(shí)現(xiàn)類(lèi) SimpleApplicationEventMulticaster 供我們直接使用,只需要將這個(gè)對(duì)象注冊(cè)到 Spring 容器即可。

@Configuration
public class AsynchronousSpringEventsConfig {
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster
                = new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

這里 ApplicationEventMulticasterBean 需要一個(gè) java.util.concurrent.Executor對(duì)象作為事件處理的線程池,我們直接使用 Spring 提供的 SimpleAsyncTaskExecutor 對(duì)象,每次事件處理都會(huì)有創(chuàng)建新的線程。

注意:注冊(cè) ApplicationEventMulticaster Bean 后所有的事件偵聽(tīng)處理都會(huì)變成的異步形式,如果需要針對(duì)特定的事件偵聽(tīng)采用異步方式的話:可以使用 @EventListener@Async 組合來(lái)實(shí)現(xiàn)。(前提是 Spring 程序啟用 @EnableAsync 注解)

這里再提下使用異步方式處理事件的利弊,好處在于讓我們程序在處理事件更加有效率,而缺點(diǎn)就在針對(duì)異常發(fā)生的處理更加復(fù)雜,需要借助 AsyncUncaughtExceptionHandler接口實(shí)現(xiàn)。

2.3 Spring Boot 事件與偵聽(tīng)

學(xué)習(xí)了那么多 Spring Framework 的事件處理相關(guān)的內(nèi)容后,我們現(xiàn)在再來(lái)看看在 Spring Boot 里事件處理有什么需要額外學(xué)習(xí)的地方。還是一樣,我們先從 Spring Boot 官方文檔下手,在 Spring Boot

Doc 的 23.5 Application Events and Listeners 一節(jié)中提到了事件處理:

  • In addition to the usual Spring Framework events, such as ContextRefreshedEvent, a SpringApplication sends some additional application events.

  • Application events are sent by using Spring Framework’s event publishing mechanism.

可以看出 Spring Boot 仍是基于 Spring Framework 的事件發(fā)布機(jī)制去處理事件,只是在此基礎(chǔ)了新增了幾個(gè) SpringApplication 相關(guān)的事件:

  • ApplicationStartingEvent :程序啟動(dòng)時(shí)發(fā)生。
  • ApplicationEnvironmentPreparedEvent :程序中Environment 對(duì)象就緒時(shí)發(fā)生。
  • ApplicationPreparedEvent :程序啟動(dòng)后但還未刷新時(shí)發(fā)生。
  • ApplicationStartedEvent:程序啟動(dòng)刷新后發(fā)生。
  • ApplicationReadyEvent:程序啟動(dòng)完畢,等待請(qǐng)求時(shí)發(fā)生。
  • ApplicationFailedEvent :程序啟動(dòng)過(guò)程中出現(xiàn)異常時(shí)發(fā)生。

并且它們的執(zhí)行順序也是列舉書(shū)順序依次觸發(fā)的。

另外,需要注意的是,當(dāng)需要觸發(fā)的事件是在 ApplicationContext 創(chuàng)建之前發(fā)生時(shí),用 @Bean 方式注冊(cè)的偵聽(tīng)器就不會(huì)執(zhí)行,而 Spring Boot 為此提供了三種方式來(lái)處理這種情況:

  1. 使用 SpringApplication.addListeners(…) 方法注冊(cè)偵聽(tīng)器

    SpringApplication springApplication = new SpringApplication(SpringEventsApplication.class);
    springApplication.addListeners(new NormalCustomEventListener());
    springApplication.run(args);
    
  2. 使用 SpringApplicationBuilder.listeners(…)方法注冊(cè)偵聽(tīng)器

    SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(SpringEventsApplication.class);
    springApplicationBuilder.listeners(new NormalCustomEventListener()).run(args);
    
  3. 在應(yīng)用資源文件夾新建文件 META-INF/spring.factories,并將 org.springframework.context.ApplicationListener 作為鍵,指定需要注冊(cè)的偵聽(tīng)器類(lèi),如:

    org.springframework.context.ApplicationListener=\
    com.one.learn.spring.springevents.listener.NormalSecondCutomEventListener
    

3 結(jié)語(yǔ)

到這里我們學(xué)習(xí) Spring 事件相關(guān)的內(nèi)容就結(jié)束了,了解 Spring 的事件機(jī)制,并適當(dāng)應(yīng)用,可以為我們完成程序的某個(gè)功能時(shí)提供一個(gè)更加解耦,靈活的實(shí)現(xiàn)方式。

如果讀完覺(jué)得有收獲的話,歡迎點(diǎn)【好看】,點(diǎn)擊文章頭圖,掃碼關(guān)注【聞人的技術(shù)博客】??????。

4 參考

Spring context-functionality-events: https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#context-functionality-events

Spring boot-features-application-events-and-listeners:https://docs.spring.io/spring-boot/docs/2.1.4.RELEASE/reference/htmlsingle/#boot-features-application-events-and-listeners

Spring Expression Language: https://docs.spring.io/spring/docs/4.3.10.RELEASE/spring-framework-reference/html/expressions.html

SpringEvents: https://www.baeldung.com/spring-events

Better application events in Spring Framework 4.2: https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容