掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文主要是簡(jiǎn)單的講述了Spring的事件機(jī)制,基本概念,講述了事件機(jī)制的三要素事件、事件發(fā)布、事件監(jiān)聽(tīng)器。如何實(shí)現(xiàn)一個(gè)事件機(jī)制,應(yīng)用的場(chǎng)景,搭配@Async注解實(shí)現(xiàn)異步的操作等等。希望對(duì)大家有所幫助。

Spring的事件機(jī)制是Spring框架中的一個(gè)重要特性,基于觀察者模式實(shí)現(xiàn),它可以實(shí)現(xiàn)應(yīng)用程序中的解耦,提高代碼的可維護(hù)性和可擴(kuò)展性。Spring的事件機(jī)制包括事件、事件發(fā)布、事件監(jiān)聽(tīng)器等幾個(gè)基本概念。其中,事件是一個(gè)抽象的概念,它代表著應(yīng)用程序中的某個(gè)動(dòng)作或狀態(tài)的發(fā)生。事件發(fā)布是事件發(fā)生的地方,它負(fù)責(zé)產(chǎn)生事件并通知事件監(jiān)聽(tīng)器。事件監(jiān)聽(tīng)器是事件的接收者,它負(fù)責(zé)處理事件并執(zhí)行相應(yīng)的操作。在Spring的事件機(jī)制中,事件源和事件監(jiān)聽(tīng)器之間通過(guò)事件進(jìn)行通信,從而實(shí)現(xiàn)了模塊之間的解耦。
舉個(gè)例子:用戶修改密碼,修改完密碼后需要短信通知用戶,記錄關(guān)鍵性日志,等等其他業(yè)務(wù)操作。
如下圖,就是我們需要調(diào)用多個(gè)服務(wù)來(lái)進(jìn)行實(shí)現(xiàn)一個(gè)修改密碼的功能。
使用了事件機(jī)制后,我們只需要發(fā)布一個(gè)事件,無(wú)需關(guān)心其擴(kuò)展的邏輯,讓我們的事件監(jiān)聽(tīng)器去處理,從而實(shí)現(xiàn)了模塊之間的解耦。
通過(guò)繼承ApplicationEvent,實(shí)現(xiàn)自定義事件。是對(duì) Java EventObject 的擴(kuò)展,表示 Spring 的事件,Spring 中的所有事件都要基于其進(jìn)行擴(kuò)展。其源碼如下。
我們可以獲取到timestamp屬性指的是發(fā)生時(shí)間。
事件發(fā)布是事件發(fā)生的地方,它負(fù)責(zé)產(chǎn)生事件并通知事件監(jiān)聽(tīng)器。ApplicationEventPublisher用于用于發(fā)布 ApplicationEvent 事件,發(fā)布后 ApplicationListener 才能監(jiān)聽(tīng)到事件進(jìn)行處理。源碼如下。
需要一個(gè)ApplicationEvent,就是我們的事件,來(lái)進(jìn)行發(fā)布事件。
ApplicationListener 是 Spring 事件的監(jiān)聽(tīng)器,用來(lái)接受事件,所有的監(jiān)聽(tīng)器都必須實(shí)現(xiàn)該接口。該接口源碼如下。
下面會(huì)給大家演示如何去使用Spring的事件機(jī)制。就拿修改密碼作為演示。
新增一個(gè)類,繼承我們的ApplicationEvent。
如下面代碼,繼承后定義了一個(gè)userId,有一個(gè)UserChangePasswordEvent方法。這里就定義我們監(jiān)聽(tīng)器需要的業(yè)務(wù)參數(shù),監(jiān)聽(tīng)器需要那些參數(shù),我們這里就定義那些參數(shù)。
/**
* @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;
}
}實(shí)現(xiàn)監(jiān)聽(tīng)器有兩種方法
/**
* @Author JiaQIng
* @Description 發(fā)送短信監(jiān)聽(tīng)器
* @ClassName MessageListener
* @Date 2023/3/26 14:16
**/
@Component
public class MessageListener implements ApplicationListener {
@Override
public void onApplicationEvent(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開(kāi)始執(zhí)行業(yè)務(wù)操作給用戶發(fā)送短信。用戶userId為:" + event.getUserId());
}
} 代碼如下。新建一個(gè)事件監(jiān)聽(tīng)器,注入到Spring容器中,交給Spring管理。在指定方法上添加@EventListener參數(shù)為監(jiān)聽(tīng)的事件。方法為業(yè)務(wù)代碼。使用 @EventListener 注解的好處是一個(gè)類可以寫很多監(jiān)聽(tīng)器,定向監(jiān)聽(tīng)不同的事件,或者同一個(gè)事件。
/**
* @Author JiaQIng
* @Description 事件監(jiān)聽(tīng)器
* @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("開(kāi)始執(zhí)行業(yè)務(wù)操作生成關(guān)鍵日志。用戶userId為:" + event.getUserId());
}
@EventListener({ UserChangePasswordEvent.class })
public void messageListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開(kāi)始執(zhí)行業(yè)務(wù)操作給用戶發(fā)送短信。用戶userId為:" + event.getUserId());
}
}注解源碼如下:主要是看一下注釋內(nèi)容。
// 在這個(gè)注解上面有一個(gè)注解:`@EventListener`,所以表明其實(shí)這個(gè)注解也是個(gè)事件監(jiān)聽(tīng)器。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {
/**
* 這個(gè)注解取值有:BEFORE_COMMIT(指定目標(biāo)方法在事務(wù)commit之前執(zhí)行)、AFTER_COMMIT(指定目標(biāo)方法在事務(wù)commit之后執(zhí)行)、
* AFTER_ROLLBACK(指定目標(biāo)方法在事務(wù)rollback之后執(zhí)行)、AFTER_COMPLETION(指定目標(biāo)方法在事務(wù)完成時(shí)執(zhí)行,這里的完成是指無(wú)論事務(wù)是成功提交還是事務(wù)回滾了)
* 各個(gè)值都代表什么意思表達(dá)什么功能,非常清晰,
* 需要注意的是:AFTER_COMMIT + AFTER_COMPLETION是可以同時(shí)生效的
* AFTER_ROLLBACK + AFTER_COMPLETION是可以同時(shí)生效的
*/
TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
/**
* 表明若沒(méi)有事務(wù)的時(shí)候,對(duì)應(yīng)的event是否需要執(zhí)行,默認(rèn)值為false表示,沒(méi)事務(wù)就不執(zhí)行了。
*/
boolean fallbackExecution() default false;
/**
* 這里巧妙的用到了@AliasFor的能力,放到了@EventListener身上
* 注意:一般建議都需要指定此值,否則默認(rèn)可以處理所有類型的事件,范圍太廣了。
*/
@AliasFor(annotation = EventListener.class, attribute = "classes")
Class>[] value() default {};
/**
* The event classes that this listener handles.
* 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 not declare any parameters.
*/
@AliasFor(annotation = EventListener.class, attribute = "classes")
Class>[] classes() default {};
/**
* Spring Expression Language (SpEL) attribute used for making the event
* handling conditional.
*
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事務(wù)類型,value指定事件。
/**
* @Author JiaQIng
* @Description 事件監(jiān)聽(tīng)器
* @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("開(kāi)始執(zhí)行業(yè)務(wù)操作生成關(guān)鍵日志。用戶userId為:" + event.getUserId());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT,value = { UserChangePasswordEvent.class })
public void messageListener(UserChangePasswordEvent event) {
System.out.println("收到事件:" + event);
System.out.println("開(kāi)始執(zhí)行業(yè)務(wù)操作給用戶發(fā)送短信。用戶userId為:" + event.getUserId());
}
}三種發(fā)布事件的方法,我給大家演示一下@Autowired注入的方式發(fā)布我們的事件。
@SpringBootTest
class SpirngEventApplicationTests {
@Autowired
ApplicationEventPublisher appEventPublisher;
@Test
void contextLoads() {
appEventPublisher.publishEvent(new UserChangePasswordEvent("1111111"));
}
}我們執(zhí)行一下看一下接口。
測(cè)試成功。
監(jiān)聽(tīng)器默認(rèn)是同步執(zhí)行的,如果我們想實(shí)現(xiàn)異步執(zhí)行,可以搭配@Async注解使用,但是前提條件是你真的懂@Async注解,使用不當(dāng)會(huì)出現(xiàn)問(wèn)題的。 后續(xù)我會(huì)出一篇有關(guān)@Async注解使用的文章。這里就不給大家詳細(xì)的解釋了。有想了解的同學(xué)可以去網(wǎng)上學(xué)習(xí)一下有關(guān)@Async注解使用。
使用@Async時(shí),需要配置線程池,否則用的還是默認(rèn)的線程池也就是主線程池,線程池使用不當(dāng)會(huì)浪費(fèi)資源,嚴(yán)重的會(huì)出現(xiàn)OOM事故。
下圖是阿里巴巴開(kāi)發(fā)手冊(cè)的強(qiáng)制要求。
簡(jiǎn)單的演示一下:這里聲明一下俺沒(méi)有使用線程池,只是簡(jiǎn)單的演示一下。
@EnableAsync
@SpringBootApplication
public class SpirngEventApplication {
public static void main(String[] args) {
SpringApplication.run(SpirngEventApplication.class, args);
}
}/**
* @Author JiaQIng
* @Description 事件監(jiān)聽(tīng)器
* @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("開(kāi)始執(zhí)行業(yè)務(wù)操作生成關(guān)鍵日志。用戶userId為:" + event.getUserId());
}
}這樣我們的異步執(zhí)行監(jiān)聽(tīng)器的業(yè)務(wù)操作就完成了。
此文章主要是講解什么是Spring的事件機(jī)制,怎么使用Spring事件機(jī)制,工作中的場(chǎng)景有哪些。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流