掌握 Spring 之事件處理

頭圖

1 前言

本次咱們來學習 Spring 的事件處理,源於實際工做中遇到的項目需求:在一個支付的下單場景中,當用戶真正支付成功,服務器收到回調後就須要及時更新訂單數據狀態來保證數據一致。一般作法就是在回調方法裏直接使用訂單服務更新數據, 然而這樣實現上兩個模塊出現了緊密耦合,若是訂單更新的操做須要進行調整,那麼在支付回調的代碼塊中也須要被修改。html

爲了不這樣狀況發生,我採用了 Spring 事件發佈與訂閱的方式來實現接受支付回調,發佈通知更新訂單狀態的這個功能,讓訂單服務更新數據的操做只依賴特定的事件,而不用關心具體的觸發對象,也能達到代碼複用的目的。java

本文主要內容涉及以下:git

  • Spring 標準事件的處理
  • Spring 中自定義事件擴展實現
  • Spring Boot 的事件與偵聽

示例項目:github

環境支持:web

  • JDK 8
  • SpringBoot 2.1.4
  • Maven 3.6.0

2.1 Spring 標準事件處理

Spring 程序啓動過程當中會有不一樣的事件通知,內置標準的事件有 5 種:spring

事件 說明
ContextRefreshedEvent 當 Spring 容器處於初始化或者刷新階段時就會觸發,事實是ApplicationContext#refresh()方法被調用時,此時容器已經初始化完畢。
ContextStartedEvent 當調用 ConfigurableApplicationContext接口下的 start() 方法時觸發,表示 Spring 容器啓動;一般用於 Spring 容器顯式關閉後的啓動。
ContextStoppedEvent 當調用 ConfigurableApplicationContext 接口下的 stop()方法時觸發,表示 Spring 容器中止,此時能經過其 start()方法重啓容器。
ContextClosedEvent 當 Spring 容器調用 ApplicationContext#close() 方法時觸發,此時 Spring 的 beans 都已經被銷燬,而且不會從新啓動和刷新。
RequestHandledEvent 只在 Web 應用下存在,當接受到 HTTP 請求並處理後就會觸發,實際傳遞的默認實現類 ServletRequestHandledEvent

一般狀況下,Spring 程序都會接收到 ContextRefreshedEvent, ContextClosedEvent 事件的通知。數據庫

知道了 Spring 自帶的事件有哪些後,咱們就能夠針對一些場景利用事件機制來實現需求,好比說在 Spring 啓動後初始化資源,加載緩存數據到內存中等等。代碼實現也很簡單,以下:express

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

// 啓動 Spring 程序後,控制檯出現以下日誌:
// Spring 容器啓動 獲取到 Application Context 對象 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6950ed69, started on Sun May 26 12:19:33 CST 2019
複製代碼

咱們能夠從 ContextRefreshedEvent 事件中獲取到 ApplicationContext 對象,從而獲取 Spring 容器中任何已經裝載的 Bean 進行自定義的操做。api

2.1.1 註解驅動的事件偵聽

引入 @EventListener

從 Spring 4.2 開始,Spring 又提供了更靈活的,註解驅動的事件偵聽處理方式。主要使用 @EventListener 註解來標記須要監聽程序事件的方法,底層由 EventListenerMethodProcessor 對象將標註的方法轉爲成 ApplicationListener 實例。數組

爲何說這個註解方式偵聽事件更加靈活呢,咱們能夠先看下 @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 註解主要有兩個屬性:classesconditionclasses 表示所須要偵聽的事件類型,是個數組,因此容許在單個方法裏進行多個不一樣事件的偵聽,以此作到複用的效果;condition 顧名思義就是用來定義所偵聽事件是否處理的前置條件,這裏須要注意的是使用 Spring Expression Language (SpEL)定義條件,好比 #root.event 表示了具體的 ApplicationEvent對象, 使用方式能夠參考下方示例代碼:

@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標記的方法參數類型再也不限制必須是 ApplicationEvent的子類,沒有實現 ApplicationListener 接口方法的約束,也讓事件變得更加靈活。

事件的傳遞

另外,使用 @EventListener 還支持事件的傳遞,將當前事件處理好的結果封裝後發佈一個新的事件,實現的方式就是讓偵聽方法返回非 null 值時,就視爲事件繼續傳播,以下面的示例代碼:

@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 偵聽器優先級

當咱們對單個事件存在多個偵聽器時,可能會因爲需求想要指定偵聽器的執行順序,這一點 Spring 也爲咱們考慮到了,只要使用 @Order註解聲明監聽類或者監聽方法便可,根據 @Ordervalue 大小來肯定執行順序,越小越優先執行。

@EventListener
@Order(42)
public void processEvent(Event event) {
}
複製代碼

2.2 自定義事件

在瞭解如何偵聽 Spring 事件後,咱們再來看下如何實現自定義的事件發佈和偵聽處理。首先就要介紹 Spring 中事件機制的三類對象:

  • Event :所須要觸發的具體事件對象,一般擴展 ApplicationEvent 實現。
  • Publisher:觸發事件發佈的對象,Spring 提供了 ApplicationEventPublisher 對象供咱們使用,使用它的publishEvent() 方法就能夠發佈該事件。
  • Listener:偵聽事件發生的對象,也就是接受回調進行處理的地方,能夠經過 實現 ApplicationListener接口,或者使用前面提到的 @EventListener註解聲明爲事件的偵聽器。

接下來就簡單看下,一個自定義事件從聲明到發佈訂閱的代碼示例。

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("開始發佈事件 " + message);
        applicationEventPublisher.publishEvent(new CustomEvent(this, message));
    }
}
複製代碼

建立事件發佈者有兩種方式,一種是使用 @Autowire註解,經過 Spring 容器的依賴注入功能,直接注入 ApplicationEventPublisher對象,或者實現 ApplicationEventPublisherAware接口,在 Spring 容器啓動時由 Spring 設置。

2.2.3 自定義 Listener

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

定義事件偵聽器時,咱們經過實現 ApplicationListener 接口,指定了事件類型,這樣在處理事件時就不避免了事件類型判斷和轉換。

關於事件偵聽器還須要注意的一點是:Spring 事件處理默認是同步的,這一點在 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.

當發佈者執行了 publishEvent() 方法,默認狀況下方法所在的當前線程就會阻塞,直到全部該事件相關的偵聽器將事件處理完成。而這樣採用單線程同步方式處理的好處主要是能夠保證讓事件處理與發佈者處於同一個事務環境裏,若是多個偵聽方法涉及到數據庫操做時保證了事務的存在。

2.2.4 異步事件處理

固然 Spring 也提供了異步偵聽事件的方式,這裏主要依賴 ApplicationEventMulticaster接口,能夠理解爲廣播方式,爲了便於使用,Spring 提供一個簡易的實現類 SimpleApplicationEventMulticaster 供咱們直接使用,只須要將這個對象註冊到 Spring 容器便可。

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

這裏 ApplicationEventMulticasterBean 須要一個 java.util.concurrent.Executor對象做爲事件處理的線程池,咱們直接使用 Spring 提供的 SimpleAsyncTaskExecutor 對象,每次事件處理都會有建立新的線程。

注意:註冊 ApplicationEventMulticaster Bean 後全部的事件偵聽處理都會變成的異步形式,若是須要針對特定的事件偵聽採用異步方式的話:可使用 @EventListener@Async 組合來實現。(前提是 Spring 程序啓用 @EnableAsync 註解)

這裏再提下使用異步方式處理事件的利弊,好處在於讓咱們程序在處理事件更加有效率,而缺點就在針對異常發生的處理更加複雜,須要藉助 AsyncUncaughtExceptionHandler接口實現。

2.3 Spring Boot 事件與偵聽

學習了那麼多 Spring Framework 的事件處理相關的內容後,咱們如今再來看看在 Spring Boot 裏事件處理有什麼須要額外學習的地方。仍是同樣,咱們先從 Spring Boot 官方文檔下手,在 Spring Boot

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

  • 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 的事件發佈機制去處理事件,只是在此基礎了新增了幾個 SpringApplication 相關的事件:

  • ApplicationStartingEvent :程序啓動時發生。
  • ApplicationEnvironmentPreparedEvent :程序中Environment 對象就緒時發生。
  • ApplicationPreparedEvent :程序啓動後但還未刷新時發生。
  • ApplicationStartedEvent:程序啓動刷新後發生。
  • ApplicationReadyEvent:程序啓動完畢,等待請求時發生。
  • ApplicationFailedEvent :程序啓動過程當中出現異常時發生。

而且它們的執行順序也是列舉書順序依次觸發的。

另外,須要注意的是,當須要觸發的事件是在 ApplicationContext 建立以前發生時,用 @Bean 方式註冊的偵聽器就不會執行,而 Spring Boot 爲此提供了三種方式來處理這種狀況:

  1. 使用 SpringApplication.addListeners(…) 方法註冊偵聽器

    SpringApplication springApplication = new SpringApplication(SpringEventsApplication.class);
    springApplication.addListeners(new NormalCustomEventListener());
    springApplication.run(args);
    複製代碼
  2. 使用 SpringApplicationBuilder.listeners(…)方法註冊偵聽器

    SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(SpringEventsApplication.class);
    springApplicationBuilder.listeners(new NormalCustomEventListener()).run(args);
    複製代碼
  3. 在應用資源文件夾新建文件 META-INF/spring.factories,並將 org.springframework.context.ApplicationListener 做爲鍵,指定須要註冊的偵聽器類,如:

    org.springframework.context.ApplicationListener=\
    com.one.learn.spring.springevents.listener.NormalSecondCutomEventListener
    複製代碼

3 結語

到這裏咱們學習 Spring 事件相關的內容就結束了,瞭解 Spring 的事件機制,並適當應用,能夠爲咱們完成程序的某個功能時提供一個更加解耦,靈活的實現方式。

若是讀完以爲有收穫的話,歡迎點【好看】,點擊文章頭圖,掃碼關注【聞人的技術博客】😄😄😄。

4 參考

Spring context-functionality-events: docs.spring.io/spring/docs…

Spring boot-features-application-events-and-listeners:docs.spring.io/spring-boot…

Spring Expression Language: docs.spring.io/spring/docs…

SpringEvents: www.baeldung.com/spring-even…

Better application events in Spring Framework 4.2: spring.io/blog/2015/0…

相關文章
相關標籤/搜索