Spring事件釋出與監聽機制

陳皮的JavaLib發表於2021-06-27

我是陳皮,一個在網際網路 Coding 的 ITer,微信搜尋「陳皮的JavaLib」第一時間閱讀最新文章,回覆【資料】,即可獲得我精心整理的技術資料,電子書籍,一線大廠面試資料和優秀簡歷模板。

前言


Spring 提供了 ApplicationContext 事件機制,可以釋出和監聽事件,這個特性非常有用。

Spring 內建了一些事件和監聽器,例如在 Spring 容器啟動前,Spring 容器啟動後,應用啟動失敗後等事件發生後,監聽在這些事件上的監聽器會做出相應的響應處理。

當然,我們也可以自定義監聽器,監聽 Spring 原有的事件。或者自定義我們自己的事件和監聽器,在必要的時間點發布事件,然後監聽器監聽到事件就做出響應處理。


ApplicationContext 事件機制


ApplicationContext 事件機制採用觀察者設計模式來實現,通過 ApplicationEvent 事件類和 ApplicationListener 監聽器介面,可以實現 ApplicationContext 事件釋出與處理。

每當 ApplicationContext 釋出 ApplicationEvent 時,如果 Spring 容器中有 ApplicationListener bean,則監聽器會被觸發執行相應的處理。當然,ApplicationEvent 事件的釋出需要顯示觸發,要麼 Spring 顯示觸發,要麼我們顯示觸發。


ApplicationListener 監聽器


定義應用監聽器需要實現的介面。此介面繼承了 JDK 標準的事件監聽器介面 EventListener,EventListener 介面是一個空的標記介面,推薦所有事件監聽器必須要繼承它。

package org.springframework.context;

import java.util.EventListener;

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	/**
	 * 處理應用事件
	 */
	void onApplicationEvent(E event);
}
package java.util;

public interface EventListener {
}

ApplicationListener 是個泛型介面,我們自定義此介面的實現類時,如果指定了泛型的具體事件類,那麼只會監聽此事件。如果不指定具體的泛型,則會監聽 ApplicationEvent 抽象類的所有子類事件。

如下我們定義一個監聽器,監聽具體的事件,例如監聽 ApplicationStartedEvent 事件。

package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        log.info(">>> MyApplicationListener:{}", event);
    }
}

啟動服務,會發現在服務啟動後,此監聽器被觸發了。

在這裡插入圖片描述

如果不指定具體的泛型類,則會監聽 ApplicationEvent 抽象類的所有子類事件。如下所示:

package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        log.info(">>> MyApplicationListener:{}", event);
    }
}

在這裡插入圖片描述

注意,監聽器類的 bean 要注入到 Spring 容器中,不然不會生效。一種是使用註解注入,例如 @Component。另外可以使用 SpringApplicationBuilder.listeners() 方法新增,不過這兩種方式有區別的,看以下示例。

首先我們使用 @Component 註解方式,服務啟動時,監視到了2個事件:

  • ApplicationStartedEvent
  • ApplicationReadyEvent
package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener implements ApplicationListener<SpringApplicationEvent> {
    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
        log.info(">>> MyApplicationListener:{}", event);
    }
}

在這裡插入圖片描述

而使用 SpringApplicationBuilder.listeners() 方法新增監聽器,服務啟動時,監聽到了5個事件:

  • ApplicationEnvironmentPreparedEvent
  • ApplicationContextInitializedEvent
  • ApplicationPreparedEvent
  • ApplicationStartedEvent
  • ApplicationReadyEvent
package com.chenpi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

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

        SpringApplication app = new SpringApplicationBuilder(Application.class)
                .listeners(new MyApplicationListener()).build();
        app.run(args);
    }
}

在這裡插入圖片描述

其實這是和監聽器 bean 註冊的時機有關,@component 註解的監聽器 bean 只有在 bean 初始化註冊完後才能使用;而通過 SpringApplicationBuilder.listeners() 新增的監聽器 bean 是在容器啟動前,所以監聽到的事件比較多。但是注意,這兩個不要同時使用,不然監聽器會重複執行兩遍。

如果你想在監聽器 bean 中注入其他 bean(例如 @Autowired),那最好是使用註解形式,因為如果太早釋出監聽器,可能其他 bean 還未初始化完成,可能會報錯。


ApplicationEvent 事件


ApplicationEvent 是所有應用事件需要繼承的抽象類。它繼承了 EventObject 類,EventObject 是所有事件的根類,這個類有個 Object 型別的物件 source,代表事件源。所有繼承它的類的建構函式都必須要顯示傳遞這個事件源。

package org.springframework.context;

import java.util.EventObject;

public abstract class ApplicationEvent extends EventObject {

	private static final long serialVersionUID = 7099057708183571937L;

	// 釋出事件的系統時間
	private final long timestamp;

	public ApplicationEvent(Object source) {
		super(source);
		this.timestamp = System.currentTimeMillis();
	}

	public final long getTimestamp() {
		return this.timestamp;
	}
}
package java.util;

public class EventObject implements java.io.Serializable {

    private static final long serialVersionUID = 5516075349620653480L;

    protected transient Object  source;

    public EventObject(Object source) {
        if (source == null)
            throw new IllegalArgumentException("null source");

        this.source = source;
    }

    public Object getSource() {
        return source;
    }

    public String toString() {
        return getClass().getName() + "[source=" + source + "]";
    }
}

在 Spring 中,比較重要的事件類是 SpringApplicationEvent。Spring 有一些內建的事件,當完成某種操作時會觸發某些事件。這些內建事件繼承 SpringApplicationEvent 抽象類。SpringApplicationEvent 繼承 ApplicationEvent 並增加了字串陣列引數欄位 args。

/**
 * Base class for {@link ApplicationEvent} related to a {@link SpringApplication}.
 *
 * @author Phillip Webb
 * @since 1.0.0
 */
@SuppressWarnings("serial")
public abstract class SpringApplicationEvent extends ApplicationEvent {

	private final String[] args;

	public SpringApplicationEvent(SpringApplication application, String[] args) {
		super(application);
		this.args = args;
	}

	public SpringApplication getSpringApplication() {
		return (SpringApplication) getSource();
	}

	public final String[] getArgs() {
		return this.args;
	}
}

在這裡插入圖片描述

我們可以編寫自己的監聽器,然後監聽這些事件,實現自己的業務邏輯。例如編寫 ApplicationListener 介面的實現類,監聽 ContextStartedEvent 事件,當應用容器 ApplicationContext 啟動時,會發布該事件,所以我們編寫的監聽器會被觸發。

  • ContextRefreshedEvent:ApplicationContext 被初始化或重新整理時,事件被髮布。ConfigurableApplicationContext介面中的 refresh() 方法被呼叫也會觸發事件釋出。初始化是指所有的 Bean 被成功裝載,後處理 Bean 被檢測並啟用,所有單例 Bean 被預例項化,ApplicationContext 容器已就緒可用。
  • ContextStartedEvent:應用程式上下文被重新整理後,但在任何 ApplicationRunner 和 CommandLineRunner 被呼叫之前,釋出此事件。
  • ApplicationReadyEvent:此事件會盡可能晚地被髮布,以表明應用程式已準備好為請求提供服務。事件源是SpringApplication 本身,但是要注意修改它的內部狀態,因為到那時所有初始化步驟都已經完成了。
  • ContextStoppedEvent:ConfigurableApplicationContext 介面的 stop() 被呼叫停止 ApplicationContext 時,事件被髮布。
  • ContextClosedEvent:ConfigurableApplicationContext 介面的 close() 被呼叫關閉 ApplicationContext 時,事件被髮布。注意,一個已關閉的上下文到達生命週期末端後,它不能被重新整理或重啟。
  • ApplicationFailedEvent:當應用啟動失敗後釋出事件。
  • ApplicationEnvironmentPreparedEvent:事件是在 SpringApplication 啟動時釋出的,並且首次檢查和修改 Environment 時,此時上 ApplicationContext 還沒有建立。
  • ApplicationPreparedEvent:事件釋出時,SpringApplication 正在啟動,ApplicationContext 已經完全準備好,但沒有重新整理。在這個階段,將載入 bean definitions 並準備使用 Environment。
  • RequestHandledEvent:這是一個 web 事件,只能應用於使用 DispatcherServlet 的 Web 應用。在使用 Spring 作為前端的 MVC 控制器時,當 Spring 處理使用者請求結束後,系統會自動觸發該事件。

自定義事件和監聽器


前面介紹了自定義監聽器,然後監聽 Spring 原有的事件。下面介紹自定義事件和自定義監聽器,然後在程式中釋出事件,觸發監聽器執行,實現自己的業務邏輯。

首先自定義事件,繼承 ApplicationEvent,當然事件可以自定義自己的屬性。

package com.chenpi;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.context.ApplicationEvent;

/**
 * @Description 自定義事件
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Getter
@Setter
public class MyApplicationEvent extends ApplicationEvent {

    // 事件可以增加自己的屬性
    private String myField;

    public MyApplicationEvent(Object source, String myField) {
        // 繫結事件源
        super(source);
        this.myField = myField;
    }

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

然後自定義監聽器,監聽我們自定義的事件。

package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;

/**
 * @Description 自定義監聽器
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
public class MyApplicationListener implements ApplicationListener<MyApplicationEvent> {
    @Override
    public void onApplicationEvent(MyApplicationEvent event) {
        log.info(">>> MyApplicationListener:{}", event);
    }
}

註冊監聽器和釋出事件。註冊監聽器上面講解了有兩種方式。事件的釋出可以通過 ApplicationEventPublisher.publishEvent() 方法。此處演示直接用 configurableApplicationContext 釋出,它實現了 ApplicationEventPublisher 介面。

package com.chenpi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        // SpringApplication.run(Application.class, args);
        // 註冊監聽器
        SpringApplication app = new SpringApplicationBuilder(Application.class)
                .listeners(new MyApplicationListener()).build();
        ConfigurableApplicationContext configurableApplicationContext = app.run(args);
        // 方便演示,在專案啟動後釋出事件,當然也可以在其他操作和其他時間點發布事件
        configurableApplicationContext
                .publishEvent(new MyApplicationEvent("我是事件源,專案啟動成功後釋出事件", "我是自定義事件屬性"));
    }
}

啟動服務,結果顯示確實監聽到釋出的事件了。

2021-06-26 16:15:09.584  INFO 10992 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2021-06-26 16:15:09.601  INFO 10992 --- [           main] com.chenpi.Application                   : Started Application in 2.563 seconds (JVM running for 4.012)
2021-06-26 16:15:09.606  INFO 10992 --- [           main] com.chenpi.MyApplicationListener         : >>> MyApplicationListener:MyApplicationEvent{myField='我是自定義事件屬性', source=我是事件源,專案啟動成功後釋出事件}

事件監聽機制能達到分發,解耦效果。例如可以在業務類中釋出事件,讓監聽在此事件的監聽器執行自己的業務處理。例如:

package com.chenpi;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Service
public class MyService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

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

    public void testEvent() {
        applicationEventPublisher
                .publishEvent(new MyApplicationEvent("我是事件源", "我是自定義事件屬性"));
    }
}

註解式監聽器


除了實現 ApplicationListener 介面建立監聽器外,Spring 還提供了註解 @EventListener 來建立監聽器。

package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * @Description 自定義監聽器
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener01 {
    @EventListener
    public void onApplicationEvent(MyApplicationEvent event) {
        log.info(">>> MyApplicationListener:{}", event);
    }
}

而且註解還可以通過條件過濾只監聽指定條件的事件。例如事件的 myField 屬性的值等於"陳皮"的事件。

package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * @Description 自定義監聽器
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener01 {
    @EventListener(condition = "#event.myField.equals('陳皮')")
    public void onApplicationEvent(MyApplicationEvent event) {
        log.info(">>> MyApplicationListener:{}", event);
    }
}

還可以在同一個類中定義多個監聽,對同一個事件的不同監聽還可以指定順序。order 值越小越先執行。

package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * @Description 自定義監聽器
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener01 {
    @Order(2)
    @EventListener
    public void onApplicationEvent(MyApplicationEvent event) {
        log.info(">>> onApplicationEvent order=2:{}", event);
    }

    @Order(1)
    @EventListener
    public void onApplicationEvent01(MyApplicationEvent event) {
        log.info(">>> onApplicationEvent order=1:{}", event);
    }

    @EventListener
    public void otherEvent(YourApplicationEvent event) {
        log.info(">>> otherEvent:{}", event);
    }
}

執行結果如下:

>>> onApplicationEvent order=1:MyApplicationEvent{myField='陳皮', source=我是事件源}
>>> onApplicationEvent order=2:MyApplicationEvent{myField='陳皮', source=我是事件源}
>>> otherEvent:MyApplicationEvent{myField='我是自定義事件屬性01', source=我是事件源01}

事件的監聽處理是同步的,如下:

package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Service
@Slf4j
public class MyService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

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

    public void testEvent() {
        log.info(">>> testEvent begin");
        applicationEventPublisher.publishEvent(new MyApplicationEvent("我是事件源", "陳皮"));
        applicationEventPublisher.publishEvent(new YourApplicationEvent("我是事件源01", "我是自定義事件屬性01"));
        log.info(">>> testEvent end");
    }
}

執行結果如下:

2021-06-26 20:34:27.990  INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyService                     : >>> testEvent begin
2021-06-26 20:34:27.990  INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01       : >>> onApplicationEvent order=1:MyApplicationEvent{myField='陳皮', source=我是事件源}
2021-06-26 20:34:27.991  INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01       : >>> onApplicationEvent order=2:MyApplicationEvent{myField='陳皮', source=我是事件源}
2021-06-26 20:34:27.992  INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01       : >>> otherEvent:MyApplicationEvent{myField='我是自定義事件屬性01', source=我是事件源01}
2021-06-26 20:34:27.992  INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyService                     : >>> testEvent end

不過,我們也可以顯示指定非同步方式去執行監聽器,記得在服務新增 @EnableAsync 註解開啟非同步註解。

package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * @Description 自定義監聽器
 * @Author 陳皮
 * @Date 2021/6/26
 * @Version 1.0
 */
@Slf4j
@Component
public class MyApplicationListener01 {

    @Async
    @Order(2)
    @EventListener
    public void onApplicationEvent(MyApplicationEvent event) {
        log.info(">>> onApplicationEvent order=2:{}", event);
    }

    @Order(1)
    @EventListener
    public void onApplicationEvent01(MyApplicationEvent event) {
        log.info(">>> onApplicationEvent order=1:{}", event);
    }

    @Async
    @EventListener
    public void otherEvent(YourApplicationEvent event) {
        log.info(">>> otherEvent:{}", event);
    }
}

執行結果如下,注意列印的執行緒名。

2021-06-26 20:37:04.807  INFO 9092 --- [nio-8081-exec-1] com.chenpi.MyService                     : >>> testEvent begin
2021-06-26 20:37:04.819  INFO 9092 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01       : >>> onApplicationEvent order=1:MyApplicationEvent{myField='陳皮', source=我是事件源}
2021-06-26 20:37:04.831  INFO 9092 --- [         task-1] com.chenpi.MyApplicationListener01       : >>> onApplicationEvent order=2:MyApplicationEvent{myField='陳皮', source=我是事件源}
2021-06-26 20:37:04.831  INFO 9092 --- [nio-8081-exec-1] com.chenpi.MyService                     : >>> testEvent end
2021-06-26 20:37:04.831  INFO 9092 --- [         task-2] com.chenpi.MyApplicationListener01       : >>> otherEvent:MyApplicationEvent{myField='我是自定義事件屬性01', source=我是事件源01}

相關文章