使用Spring Event實現(xiàn)內(nèi)部模塊間的輕松解耦
Spring Event 是 Spring 框架提供的一個核心組件,其允許服務(wù)內(nèi)部不同模塊之間通過觀察者模式(發(fā)布-訂閱模式)進行通信,從而實現(xiàn)模塊間的解耦。
即 Spring Event 是一種事件驅(qū)動的編程模型,一個模塊在做完一件事后,無需直接調(diào)用其它模塊處理后續(xù)邏輯,而是發(fā)布一個事件出來,由其它對該事件感興趣的模塊訂閱并處理這個事件,事件發(fā)布者無需關(guān)注訂閱者是誰,從而實現(xiàn)模塊間的輕松解耦。
Spring Event 的使用非常的廣泛,包含但不限于:用戶注冊成功后的后續(xù)操作(如發(fā)送歡迎郵件、發(fā)放新用戶優(yōu)惠券、初始化積分等);訂單狀態(tài)的變更通知(訂單支付成功后通知庫存系統(tǒng)減庫存、通知物流系統(tǒng)準(zhǔn)備發(fā)貨等);系統(tǒng)日志記錄(重要數(shù)據(jù)被修改后通知日志系統(tǒng)記錄變更字段、通知管理員進行安全檢查和審計等)。
除了上述業(yè)務(wù)場景外,Spring Event 有時還能很巧妙的解決一些技術(shù)問題:比如解決 Spring 父子模塊的通信。實際項目中,Spring Boot 工程通常會依賴一個 SharedModule 父模塊,這個 SharedModule 父模塊里擁有一些公共的 POJO 類、數(shù)據(jù)庫 Entity 類、Util 類等,如果想在父模塊里調(diào)用子模塊的方法來實現(xiàn)一些邏輯,在技術(shù)上是有一點難度的。但通過借助 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 都可以充當(dāng)事件實體類。
下面即是我們定義的用戶注冊后事件:
package com.example.demo.model;
@Builder
@Data
public class UserRegisteredEvent {
private String email;
private String username;
}
1.2 發(fā)布事件(Event Publisher)
下面即是事件發(fā)布者 UserServiceImpl 在保存 User 后,調(diào)用 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)聽,下面的郵件服務(wù)、優(yōu)惠券服務(wù)監(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() 方法被調(diào)用后,控制臺打印了如下三行日志:
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 默認(rèn)是同步執(zhí)行的,即 ApplicationEventPublisher 的 publishEvent() 方法是阻塞式的,會等待所有監(jiān)聽器將事件處理完畢才算調(diào)用結(jié)束。
這樣,若訂閱者的處理邏輯很耗時的話會影響到發(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ā)布者在主線程調(diào)用 publishEvent() 方法后立即返回,而監(jiān)聽器的邏輯處理會在新啟動的線程中執(zhí)行,不會再阻塞主線程。
3 小結(jié)
綜上,本文首先介紹了 Spring Event 的功能,然后以「用戶注冊成功后發(fā)送郵件、發(fā)放優(yōu)惠券」為例,用 Spring Boot 示例程序的方式演示了 Spring Event 的使用。
經(jīng)過本文的實踐,說明借助 Spring Event 的確可以很輕松的實現(xiàn)服務(wù)內(nèi)部模塊間的解耦。但若是服務(wù)間的通信和解耦,以及需要更高的并發(fā)和可靠性,則還是需要引入其它第三方消息隊列等工具來實現(xiàn)。
以上就是使用Spring Event實現(xiàn)內(nèi)部模塊間的輕松解耦的詳細(xì)內(nèi)容,更多關(guān)于Spring Event內(nèi)部模塊間解耦的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot2零基礎(chǔ)到精通之?dāng)?shù)據(jù)庫專項精講
SpringBoot是一種整合Spring技術(shù)棧的方式(或者說是框架),同時也是簡化Spring的一種快速開發(fā)的腳手架,本篇我們來學(xué)習(xí)如何連接數(shù)據(jù)庫進行操作2022-03-03
Java使用Lambda表達(dá)式查找list集合中是否包含某值問題
Java使用Lambda表達(dá)式查找list集合中是否包含某值的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06
Java多線程通訊之wait,notify的區(qū)別詳解
這篇文章主要介紹了Java多線程通訊之wait,notify的區(qū)別詳解,非常不錯,具有一定的參考借鑒借鑒價值,需要的朋友可以參考下2018-07-07
高并發(fā)下如何避免重復(fù)數(shù)據(jù)產(chǎn)生技巧
這篇文章主要為大家介紹了高并發(fā)下如何避免重復(fù)數(shù)據(jù)的產(chǎn)生技巧詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07
Java中ByteArrayInputStream和ByteArrayOutputStream用法詳解
這篇文章主要介紹了Java中ByteArrayInputStream和ByteArrayOutputStream用法詳解,?ByteArrayInputStream?的內(nèi)部額外的定義了一個計數(shù)器,它被用來跟蹤?read()?方法要讀取的下一個字節(jié)2022-06-06
Spring主配置文件(applicationContext.xml) 導(dǎo)入約束詳解
在本篇文章里我們給各位整理的是關(guān)于Spring主配置文件(applicationContext.xml) 導(dǎo)入約束的相關(guān)知識點內(nèi)容,需要參考下。2019-08-08
5分鐘快速學(xué)會spring boot整合JdbcTemplate的方法
這篇文章主要給大家介紹了如何通過5分鐘快速學(xué)會spring boot整合JdbcTemplate的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用spring boot整合JdbcTemplate具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
java多線程并發(fā)中使用Lockers類將多線程共享資源鎖定
Lockers在多線程編程里面一個重要的概念是鎖定,如果一個資源是多個線程共享的,為了保證數(shù)據(jù)的完整性,在進行事務(wù)性操作時需要將共享資源鎖定,這樣可以保證在做事務(wù)性操作時只有一個線程能對資源進行操作,下面看一個示例2014-01-01

