使用Spring Event實現(xiàn)內部模塊間的輕松解耦
Spring Event 是 Spring 框架提供的一個核心組件,其允許服務內部不同模塊之間通過觀察者模式(發(fā)布-訂閱模式)進行通信,從而實現(xiàn)模塊間的解耦。
即 Spring Event 是一種事件驅動的編程模型,一個模塊在做完一件事后,無需直接調用其它模塊處理后續(xù)邏輯,而是發(fā)布一個事件出來,由其它對該事件感興趣的模塊訂閱并處理這個事件,事件發(fā)布者無需關注訂閱者是誰,從而實現(xiàn)模塊間的輕松解耦。
Spring Event 的使用非常的廣泛,包含但不限于:用戶注冊成功后的后續(xù)操作(如發(fā)送歡迎郵件、發(fā)放新用戶優(yōu)惠券、初始化積分等);訂單狀態(tài)的變更通知(訂單支付成功后通知庫存系統(tǒng)減庫存、通知物流系統(tǒng)準備發(fā)貨等);系統(tǒng)日志記錄(重要數(shù)據(jù)被修改后通知日志系統(tǒng)記錄變更字段、通知管理員進行安全檢查和審計等)。
除了上述業(yè)務場景外,Spring Event 有時還能很巧妙的解決一些技術問題:比如解決 Spring 父子模塊的通信。實際項目中,Spring Boot 工程通常會依賴一個 SharedModule 父模塊,這個 SharedModule 父模塊里擁有一些公共的 POJO 類、數(shù)據(jù)庫 Entity 類、Util 類等,如果想在父模塊里調用子模塊的方法來實現(xiàn)一些邏輯,在技術上是有一點難度的。但通過借助 Spring Event,父模塊只需增加一個事件,然后發(fā)布即可,在子模塊中監(jiān)聽并處理就好了。
介紹了 Spring Event 是什么以及其適用的場景外。本文將以「用戶注冊成功后發(fā)送郵件、發(fā)放優(yōu)惠券」為例,創(chuàng)建一個 Spring Boot 示例程序,來演示 Spring Event 的使用。
下面列出寫作本文時用到的 Java、Spring Boot 以及 Spring 框架的版本:
Java: 17 Spring Boot: 3.5.5 Spring Framework: 6.2.10
1 Spring Event 如何使用?
如何使用 Spring Event 呢?只有三個步驟:定義事件、發(fā)布事件、監(jiān)聽事件。
1.1 定義事件(Event)
自 Spring 4.2 后,定義事件時,無需再繼承 ApplicationEvent。任何一個普通的 Java POJO 都可以充當事件實體類。
下面即是我們定義的用戶注冊后事件:
package com.example.demo.model;
@Builder
@Data
public class UserRegisteredEvent {
private String email;
private String username;
}
1.2 發(fā)布事件(Event Publisher)
下面即是事件發(fā)布者 UserServiceImpl 在保存 User 后,調用 ApplicationEventPublisher 發(fā)布 UserRegisteredEvent 的代碼:
package com.example.demo.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
public void save(User user) {
// save user
// userRepository.save(user);
// publish event
UserRegisteredEvent event = UserRegisteredEvent.builder()
.email(user.getEmail())
.username(user.getUsername())
.build();
eventPublisher.publishEvent(event);
LOGGER.info("user registered event successfully published");
}
}
可以看到,上述代碼在 save User 后,發(fā)布了一個 UserRegisteredEvent,并打印了一段事件成功發(fā)布的日志。
1.3 監(jiān)聽事件 (Event Listener)
自 Spring 4.2 后,訂閱者要想監(jiān)聽事件,無需再實現(xiàn) ApplicationListener 接口,而只需添加一個接收 Event 對象的方法,并在方法上加上 @EventListener 注解即可。
一個事件可以被多個訂閱者監(jiān)聽,下面的郵件服務、優(yōu)惠券服務監(jiān)聽了 UserRegisteredEvent:
package com.example.demo.service.impl;
@Service
public class EmailServiceImpl implements EmailService {
@EventListener
public void handleUserRegisteredEvent(UserRegisteredEvent event) {
// send email
LOGGER.info("email successfully sent to: {}", event.getEmail());
}
}
package com.example.demo.service.impl;
@Service
public class CouponServiceImpl implements CouponService {
@EventListener
public void handleUserRegisteredEvent(UserRegisteredEvent event) {
// issue coupon
LOGGER.info("coupon successfully issued to: {}", event.getEmail());
}
}
可以看到,上述兩個 Service 實現(xiàn)類在接收到 UserRegisteredEvent 后,分別打印了一段日志來模擬郵件成功發(fā)送和優(yōu)惠券成功發(fā)放。
1.4 測試(Testing)
下面為 UserService 編寫一個單元測試類 UserServiceTest 來測試事件的發(fā)布和訂閱:
package com.example.demo;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testSave() {
User user = new User();
// user.setXxx();
userService.save(user);
}
}
運行 UserServiceTest 測試類,可以看到,UserService 的 save() 方法被調用后,控制臺打印了如下三行日志:
CouponServiceImpl : coupon successfully issued to: larry@larry.com EmailServiceImpl : email successfully sent to: larry@larry.com UserServiceImpl : user registered event successfully published
說明我們編寫的 UserRegisteredEvent 被成功發(fā)布、訂閱和處理了。
但需要注意上面日志的打印順序:事件被處理的日志打印后才打印了事件發(fā)布的日志,這說明 Spring Event 默認是同步執(zhí)行的,即 ApplicationEventPublisher 的 publishEvent() 方法是阻塞式的,會等待所有監(jiān)聽器將事件處理完畢才算調用結束。
這樣,若訂閱者的處理邏輯很耗時的話會影響到發(fā)布者的性能。所以,是否有方法讓監(jiān)聽器的處理變成異步的呢?下面請看 Spring Event 的進階用法。
2 Spring Event 進階用法
若想讓監(jiān)聽器的執(zhí)行變成異步的,可以在程序啟動類上加上 @EnableAsync 注解:
package com.example.demo;
@EnableAsync
@SpringBootApplication
public class DemoApplication {
}
然后在事件處理方法上加上 @Async 注解,即能使事件處理方法變成異步執(zhí)行。
package com.example.demo.service.impl;
@Service
public class EmailServiceImpl implements EmailService {
@Async
@EventListener
public void handleUserRegisteredEvent(UserRegisteredEvent event) {
// send email
LOGGER.info("email successfully sent to: {}", event.getEmail());
}
}
再次運行 UserServiceTest 測試類,發(fā)現(xiàn)發(fā)布者與訂閱者的日志打印順序變成了:
UserServiceImpl : user registered event successfully published CouponServiceImpl : coupon successfully issued to: larry@larry.com EmailServiceImpl : email successfully sent to: larry@larry.com
即說明事件發(fā)布者在主線程調用 publishEvent() 方法后立即返回,而監(jiān)聽器的邏輯處理會在新啟動的線程中執(zhí)行,不會再阻塞主線程。
3 小結
綜上,本文首先介紹了 Spring Event 的功能,然后以「用戶注冊成功后發(fā)送郵件、發(fā)放優(yōu)惠券」為例,用 Spring Boot 示例程序的方式演示了 Spring Event 的使用。
經過本文的實踐,說明借助 Spring Event 的確可以很輕松的實現(xiàn)服務內部模塊間的解耦。但若是服務間的通信和解耦,以及需要更高的并發(fā)和可靠性,則還是需要引入其它第三方消息隊列等工具來實現(xiàn)。
以上就是使用Spring Event實現(xiàn)內部模塊間的輕松解耦的詳細內容,更多關于Spring Event內部模塊間解耦的資料請關注腳本之家其它相關文章!
相關文章
logback FixedWindowRollingPolicy固定窗口算法重命名文件滾動策略
這篇文章主要介紹了FixedWindowRollingPolicy根據(jù)logback 固定窗口算法重命名文件滾動策略源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11
淺談mybatis 樂觀鎖實現(xiàn),解決并發(fā)問題
這篇文章主要介紹了淺談mybatis 樂觀鎖實現(xiàn),解決并發(fā)問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
mybatis-plus的多租戶不同版本實現(xiàn)的兩種方式
本文主要介紹了mybatis-plus的多租戶不同版本實現(xiàn)的兩種方式,Mybatis Plus 3.4.0版本之后多租戶的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2025-03-03
如何解決getReader() has already been called&
這篇文章主要介紹了如何解決getReader() has already been called for this request問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
Java實現(xiàn)Html保存為.mhtml文件的代碼邏輯
文章介紹了實現(xiàn)將HTML字符串保存為.mhtml文件的代碼邏輯,包括通過URL和Cookie免密獲取HTML字符串,將HTML中的圖片、CSS、JS轉換為base64字符串,刪除不需要的布局和內容,最終將替換后的HTML保存為.mhtml文件,感興趣的朋友跟隨小編一起看看吧2026-01-01
專屬于程序員的浪漫-Java輸出動態(tài)閃圖iloveyou
這篇文章主要介紹了專屬于程序員的浪漫-Java輸出動態(tài)閃圖iloveyou,具有一定參考價值,需要的朋友可以了解下。2017-11-11
IntelliJ IDEA創(chuàng)建maven多模塊項目(圖文教程)
這篇文章主要介紹了IntelliJ IDEA創(chuàng)建maven多模塊項目(圖文教程),非常具有實用價值,需要的朋友可以參考下2017-09-09

