前言
本文主要是簡單的講述了Spring的事件機制,基本概念,講述了事件機制的三要素事件、事件釋出、事件監聽器。如何實作一個事件機制,應用的場景,搭配@Async注解實作異步的操作等等。希望對大家有所幫助。
Spring的事件機制的基本概念
Spring的事件機制是Spring架構中的一個重要特性,基于觀察者模式實作,它可以實作應用程式中的解耦,提高代碼的可維護性和可擴充性。Spring的事件機制包括事件、事件釋出、事件監聽器等幾個基本概念。
其中,事件是一個抽象的概念,它代表着應用程式中的某個動作或狀态的發生。事件釋出是事件發生的地方,它負責産生事件并通知事件監聽器。事件監聽器是事件的接收者,它負責處理事件并執行相應的操作。在Spring的事件機制中,事件源和事件監聽器之間通過事件進行通信,進而實作了子產品之間的解耦。
舉個例子:使用者修改密碼,修改完密碼後需要短信通知使用者,記錄關鍵性日志,等等其他業務操作。
如下圖,就是我們需要調用多個服務來進行實作一個修改密碼的功能。
使用了事件機制後,我們隻需要釋出一個事件,無需關心其擴充的邏輯,讓我們的事件監聽器去處理,進而實作了子產品之間的解耦。
事件
通過繼承ApplicationEvent,實作自定義事件。是對 Java EventObject 的擴充,表示 Spring 的事件,Spring 中的所有事件都要基于其進行擴充。其源碼如下。
我們可以擷取到timestamp屬性指的是發生時間。
事件釋出
事件釋出是事件發生的地方,它負責産生事件并通知事件監聽器。ApplicationEventPublisher用于用于釋出 ApplicationEvent 事件,釋出後 ApplicationListener 才能監聽到事件進行處理。源碼如下。
需要一個ApplicationEvent,就是我們的事件,來進行釋出事件。
事件監聽器
ApplicationListener 是 Spring 事件的監聽器,用來接受事件,所有的監聽器都必須實作該接口。該接口源碼如下。
Spring的事件機制的使用方法
下面會給大家示範如何去使用Spring的事件機制。就拿修改密碼作為示範。
如何定義一個事件
新增一個類,繼承我們的ApplicationEvent。
如下面代碼,繼承後定義了一個userId,有一個UserChangePasswordEvent方法。這裡就定義我們監聽器需要的業務參數,監聽器需要那些參數,我們這裡就定義那些參數。
/**
* @Author JiaQIng
* @Description 修改密碼事件
* @ClassName UserChangePasswordEvent
* @Date 2023/3/26 13:55
**/
@Getter
@Setter
public class UserChangePasswordEvent extends ApplicationEvent {
private String userId;
public UserChangePasswordEvent(String userId) {
super(new Object());
this.userId = userId;
}
}
如何監聽事件
實作監聽器有兩種方法
1、建立一個類實作ApplicationListener接口,并且重寫onApplicationEvent方法。注入到Spring容器中,交給Spring管理。如下代碼。建立了一個發送短信監聽器,收到事件後執行業務操作。
/**
* @Author JiaQIng
* @Description 發送短信監聽器
* @ClassName MessageListener
* @Date 2023/3/26 14:16
**/
@Component
public class MessageListener implements ApplicationListener<UserChangePasswordEvent> {
@Override
public void onApplicationEvent(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開始執行業務操作給使用者發送短信。使用者userId為:" + event.getUserId());
}
}
2、使用 @EventListener 注解标注處理事件的方法,此時 Spring 将建立一個 ApplicationListener bean 對象,使用給定的方法處理事件。源碼如下。參數可以給指定的事件。
這裡巧妙的用到了@AliasFor的能力,放到了@EventListener身上 注意:一般建議都需要指定此值,否則預設可以處理所有類型的事件,範圍太廣了。
代碼如下。建立一個事件監聽器,注入到Spring容器中,交給Spring管理。在指定方法上添加@EventListener參數為監聽的事件。方法為業務代碼。使用 @EventListener 注解的好處是一個類可以寫很多監聽器,定向監聽不同的事件,或者同一個事件。
/**
* @Author JiaQIng
* @Description 事件監聽器
* @ClassName LogListener
* @Date 2023/3/26 14:22
**/
@Component
public class ListenerEvent {
@EventListener({ UserChangePasswordEvent.class })
public void LogListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開始執行業務操作生成關鍵日志。使用者userId為:" + event.getUserId());
}
@EventListener({ UserChangePasswordEvent.class })
public void messageListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開始執行業務操作給使用者發送短信。使用者userId為:" + event.getUserId());
}
}
3、@TransactionalEventListener來定義一個監聽器,他與@EventListener不同的就是@EventListener标記一個方法作為監聽器,他預設是同步執行,如果釋出事件的方法處于事務中,那麼事務會在監聽器方法執行完畢之後才送出。事件釋出之後就由監聽器去處理,而不要影響原有的事務,也就是說希望事務及時送出。我們就可以使用該注解來辨別。注意此注解需要spring-tx的依賴。
注解源碼如下:主要是看一下注釋内容。
// 在這個注解上面有一個注解:`@EventListener`,是以表明其實這個注解也是個事件監聽器。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {
/**
* 這個注解取值有:BEFORE_COMMIT(指定目标方法在事務commit之前執行)、AFTER_COMMIT(指定目标方法在事務commit之後執行)、
* AFTER_ROLLBACK(指定目标方法在事務rollback之後執行)、AFTER_COMPLETION(指定目标方法在事務完成時執行,這裡的完成是指無論事務是成功送出還是事務復原了)
* 各個值都代表什麼意思表達什麼功能,非常清晰,
* 需要注意的是:AFTER_COMMIT + AFTER_COMPLETION是可以同時生效的
* AFTER_ROLLBACK + AFTER_COMPLETION是可以同時生效的
*/
TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
/**
* 表明若沒有事務的時候,對應的event是否需要執行,預設值為false表示,沒事務就不執行了。
*/
boolean fallbackExecution() default false;
/**
* 這裡巧妙的用到了@AliasFor的能力,放到了@EventListener身上
* 注意:一般建議都需要指定此值,否則預設可以處理所有類型的事件,範圍太廣了。
*/
@AliasFor(annotation = EventListener.class, attribute = "classes")
Class<?>[] value() default {};
/**
* The event classes that this listener handles.
* <p>If this attribute is specified with a single value, the annotated
* method may optionally accept a single parameter. However, if this
* attribute is specified with multiple values, the annotated method
* must <em>not</em> declare any parameters.
*/
@AliasFor(annotation = EventListener.class, attribute = "classes")
Class<?>[] classes() default {};
/**
* Spring Expression Language (SpEL) attribute used for making the event
* handling conditional.
* <p>The default is {@code ""}, meaning the event is always handled.
* @see EventListener#condition
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String condition() default "";
/**
* An optional identifier for the listener, defaulting to the fully-qualified
* signature of the declaring method (e.g. "mypackage.MyClass.myMethod()").
* @since 5.3
* @see EventListener#id
* @see TransactionalApplicationListener#getListenerId()
*/
@AliasFor(annotation = EventListener.class, attribute = "id")
String id() default "";
}
使用方式如下。phase事務類型,value指定事件。
/**
* @Author JiaQIng
* @Description 事件監聽器
* @ClassName LogListener
* @Date 2023/3/26 14:22
**/
@Component
public class ListenerEvent {
@EventListener({ UserChangePasswordEvent.class })
public void logListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開始執行業務操作生成關鍵日志。使用者userId為:" + event.getUserId());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT,value = { UserChangePasswordEvent.class })
public void messageListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開始執行業務操作給使用者發送短信。使用者userId為:" + event.getUserId());
}
}
如何釋出一個事件
1、使用ApplicationContext進行釋出,由于ApplicationContext 已經繼承了 ApplicationEventPublisher ,是以可以直接使用釋出事件。源碼如下
2、直接注入我們的ApplicationEventPublisher,使用@Autowired注入一下。
三種釋出事件的方法,我給大家示範一下@Autowired注入的方式釋出我們的事件。
@SpringBootTest
class SpirngEventApplicationTests {
@Autowired
ApplicationEventPublisher appEventPublisher;
@Test
void contextLoads() {
appEventPublisher.publishEvent(new UserChangePasswordEvent("1111111"));
}
}
我們執行一下看一下接口。
測試成功。
搭配@Async注解實作異步操作
監聽器預設是同步執行的,如果我們想實作異步執行,可以搭配@Async注解使用,但是前提條件是你真的懂@Async注解,使用不當會出現問題的。後續我會出一篇有關@Async注解使用的文章。這裡就不給大家詳細的解釋了。有想了解的同學可以去網上學習一下有關@Async注解使用。
使用@Async時,需要配置線程池,否則用的還是預設的線程池也就是主線程池,線程池使用不當會浪費資源,嚴重的會出現OOM事故。
下圖是阿裡巴巴開發手冊的強制要求。
簡單的示範一下:這裡聲明一下俺沒有使用線程池,隻是簡單的示範一下。
1、在我們的啟動類上添加@EnableAsync開啟異步執行配置
@EnableAsync
@SpringBootApplication
public class SpirngEventApplication {
public static void main(String[] args) {
SpringApplication.run(SpirngEventApplication.class, args);
}
}
2、在我們想要異步執行的監聽器上添加@Async注解。
/**
* @Author JiaQIng
* @Description 事件監聽器
* @ClassName LogListener
* @Date 2023/3/26 14:22
**/
@Component
public class ListenerEvent {
@Async
@EventListener({ UserChangePasswordEvent.class })
public void logListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開始執行業務操作生成關鍵日志。使用者userId為:" + event.getUserId());
}
}
這樣我們的異步執行監聽器的業務操作就完成了。
Spring的事件機制的應用場景
- 告警操作,比喻釘釘告警,異常告警,可以通過事件機制進行解耦。
- 關鍵性日志記錄和業務埋點,比喻說我們的關鍵日志需要入庫,記錄一下操作時間,操作人,變更内容等等,可以通過事件機制進行解耦。
- 性能監控,比喻說一些接口的時長,性能友善的埋點等。可以通過事件機制進行解耦。
- .......一切與主業務無關的操作都可以通過這種方式進行解耦,常用的場景大概就上述提到的,而且很多架構的源碼都有使用這種機制,如GateWay,Spring等等。
Spring的事件機制的注意事項
- 對于同一個事件,有多個監聽器的時候,注意可以通過@Order注解指定順序,Order的value值越小,執行的優先級就越高。
- 如果釋出事件的方法處于事務中,那麼事務會在監聽器方法執行完畢之後才送出。事件釋出之後就由監聽器去處理,而不要影響原有的事務,也就是說希望事務及時送出。我們就可以 @TransactionalEventListener來定義一個監聽器。
- 監聽器預設是同步執行的,如果我們想實作異步執行,可以搭配@Async注解使用,但是前提條件是你真的懂@Async注解,使用不當會出現問題的。
- 對于同一個事件,有多個監聽器的時候,如果出現了異常,後續的監聽器就失效了,因為他是把同一個事件的監聽器add在一個集合裡面循環執行,如果出現異常,需要注意捕獲異常處理異常。
後記
此文章主要是講解什麼是Spring的事件機制,怎麼使用Spring事件機制,工作中的場景有哪些。
源碼:https://github.com/hujiaqing789/spring-boot-study.git