SpringBoot事件發(fā)布與監(jiān)聽(tīng)超詳細(xì)講解
應(yīng)用場(chǎng)景
比如: 我們現(xiàn)在的應(yīng)用是Maven聚合工程的多模塊方式開(kāi)發(fā)?,F(xiàn)在有biz模塊、bill模塊、pay模塊。biz模塊有一個(gè)用戶(hù)注冊(cè)功能,bill模塊有一個(gè)發(fā)生郵件的功能,每次用戶(hù)注冊(cè)都需要調(diào)用bill模塊的發(fā)送郵件功能。我們只需要在pom引入bill模塊的依賴(lài),注入bill模塊發(fā)送郵件的service接口,即可以完成操作。
現(xiàn)在業(yè)務(wù)有調(diào)整,pay模塊也需要完成支付業(yè)務(wù)發(fā)送郵件,也需要和上面操作一樣,操作會(huì)很費(fèi)時(shí)費(fèi)力。有沒(méi)有一種辦法可以把它們解耦出來(lái)?
答案是有的,我們可以使用事件的方式,biz模塊只要注冊(cè)成功,發(fā)布一個(gè)用戶(hù)注冊(cè)成功的事件,pay模塊支付成功,發(fā)布一個(gè)支付成功的事件。讓bill模塊監(jiān)聽(tīng)了此事件的去做剩下的發(fā)送郵件事件就好了。對(duì)于事件發(fā)布者而言,不需要關(guān)心誰(shuí)監(jiān)聽(tīng)了該事件,以此來(lái)解耦業(yè)務(wù)。注意:必須在同一個(gè)Spring容器內(nèi),不能完成跨容器的通信,跨容器的通信可以考慮中間件消息隊(duì)列
Spring的事件應(yīng)該是在3.x版本就發(fā)布的功能了,并越來(lái)越完善,其為bean和bean之間的消息通信提供了支持。
概述
ApplicationEvent以及Listener是Spring為我們提供的一個(gè)事件監(jiān)聽(tīng)、訂閱的實(shí)現(xiàn),內(nèi)部實(shí)現(xiàn)原理是觀察者設(shè)計(jì)模式,設(shè)計(jì)初衷也是為了系統(tǒng)業(yè)務(wù)邏輯之間的解耦,提高可擴(kuò)展性以及可維護(hù)性。
- ApplicationEvent就是Spring的事件接口
- ApplicationListener就是Spring的事件監(jiān)聽(tīng)器接口,所有的監(jiān)聽(tīng)器都實(shí)現(xiàn)該接口
- ApplicationEventPublisher是Spring的事件發(fā)布接口,ApplicationContext實(shí)現(xiàn)了該接口
其執(zhí)行的流程大致為:
當(dāng)一個(gè)事件源產(chǎn)生事件時(shí),它通過(guò)事件發(fā)布器ApplicationEventPublisher發(fā)布事件,然后事件廣播器ApplicationEventMulticaster會(huì)去事件注冊(cè)表ApplicationContext中找到事件監(jiān)聽(tīng)器ApplicationListnener,并且逐個(gè)執(zhí)行監(jiān)聽(tīng)器的onApplicationEvent方法,從而完成事件監(jiān)聽(tīng)器的邏輯。
自定義事件發(fā)布和監(jiān)聽(tīng)
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
自定義事件源和實(shí)體
Spring中,事件源不強(qiáng)迫繼承ApplicationEvent接口的,也就是可以直接發(fā)布任意一個(gè)對(duì)象類(lèi)(實(shí)體類(lèi),Map,List,String等任意對(duì)象類(lèi))。但內(nèi)部其實(shí)是使用PayloadApplicationEvent類(lèi)進(jìn)行包裝了一層。
@TableName("user")
@Data
public class User {
@TableId(type = IdType.ASSIGN_ID,value = "cate_id")
private String cateId;
@TableField("cate_code")
private String cateCode;
@TableField("user_name")
private String username;
@TableField("age")
private Integer age;
@TableField("sex")
private String sex;
}
public class SendEmailEvent extends ApplicationEvent {
private User user;
public SendEmailEvent(Object source, User user) {
super(source);
this.user = user;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public String toString() {
return "SendEmailEvent{" +
"user=" + user +
'}';
}
}發(fā)布事件
這里我手動(dòng)處理的事務(wù),也可以使用@Transactional處理。這里我主要模擬發(fā)布事件發(fā)生異常和監(jiān)聽(tīng)事件中發(fā)生異常的情況。
默認(rèn)情況下,發(fā)布事件和監(jiān)聽(tīng)事件都是在一個(gè)線程中同步執(zhí)行的。業(yè)務(wù)發(fā)布事件類(lèi)中,當(dāng)try代碼塊int a = 1 / 0;模擬保存用戶(hù)異常,會(huì)進(jìn)入到catch塊中回滾保存事務(wù),不會(huì)發(fā)布事件。
當(dāng)業(yè)務(wù)類(lèi)保存用戶(hù),并執(zhí)行事件發(fā)布邏輯成功,使用@EventListener的監(jiān)聽(tīng)類(lèi)中發(fā)生異常,也會(huì)到發(fā)布事件的catch塊中回滾保存的事務(wù)。即證明了發(fā)布事件和監(jiān)聽(tīng)事件都是在一個(gè)線程中同步執(zhí)行的。事務(wù)是一致性的。
@Service
@Slf4j
//@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private ApplicationEventPublisher eventPublisher;
@Resource
private DataSourceTransactionManager transactionManager;
@Override
public Boolean saveUser(User user) {
// 手動(dòng)開(kāi)啟事務(wù) start
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transaction = transactionManager.getTransaction(definition);
// 手動(dòng)開(kāi)啟事務(wù) end
log.info("[UserService] start insert saveUser");
// 保存用戶(hù)
Boolean result = false;
try {
if (Objects.nonNull(user)) {
// 保存用戶(hù)
result = this.save(user);
// 模擬異?;貪L
// int a = 1 / 0;
}
// 1、發(fā)布ApplicationEvent對(duì)象
eventPublisher.publishEvent(new SendEmailEvent(this,user));
// 2、發(fā)送郵件 (發(fā)布任意一個(gè)對(duì)象類(lèi))
eventPublisher.publishEvent(user.getUsername());
log.info("[UserService] finish insert saveUser");
// 手動(dòng)提交事務(wù) end
transactionManager.commit(transaction);
// 手動(dòng)提交事務(wù) start
}
// 異常回滾
catch (Exception e) {
log.error("saveUser異常 fail:{}", e);
// 手動(dòng)回滾事務(wù) start
transactionManager.rollback(transaction);
// 手動(dòng)回滾事務(wù) end
}
return result;
}
}監(jiān)聽(tīng)類(lèi)使用ApplicationListener方式
@Component
@Slf4j
public class CustomListener implements ApplicationListener<SendEmailEvent> {
@Override
public void onApplicationEvent(SendEmailEvent event) {
send(event.getUser().getUsername());
}
public void send(String username) {
String code = UUID.randomUUID().toString().replace("_", "").substring(0, 4);
log.info("生成驗(yàn)證碼: {}",code);
// 可能由于網(wǎng)絡(luò)帶寬,發(fā)送驗(yàn)證碼異常,模擬監(jiān)聽(tīng)事件異常
int i = 1/0;
log.info("【{}】本次登陸驗(yàn)證碼為:{},請(qǐng)?jiān)?分鐘內(nèi)完成驗(yàn)證,請(qǐng)勿將驗(yàn)證碼泄露給他人。",username,code);
}
}
監(jiān)聽(tīng)類(lèi)使用@EventListener方式
使用@EventListener的condition可以實(shí)現(xiàn)更加精細(xì)的事件監(jiān)聽(tīng),condition支持SpEL表達(dá)式,可根據(jù)事件源的參數(shù)來(lái)判斷是否監(jiān)聽(tīng)。事件源為對(duì)象,可以使用對(duì)象.屬性。如: @EventListener(condition = "#customEvent.billType=='pay'")
@EventListener(condition = "#username.equals('風(fēng)雨')")
public void sendEmail(String username) {
log.info("transactionEventListener start");
// 發(fā)生驗(yàn)證碼
send(username);
log.info("transactionEventListener finish");
}
public void send(String username) {
String code = UUID.randomUUID().toString().replace("_", "").substring(0, 4);
log.info("生成驗(yàn)證碼: {}",code);
// 可能由于網(wǎng)絡(luò)帶寬,發(fā)送驗(yàn)證碼異常,模擬監(jiān)聽(tīng)事件異常
int i = 1/0;
log.info("【{}】本次登陸驗(yàn)證碼為:{},請(qǐng)?jiān)?分鐘內(nèi)完成驗(yàn)證,請(qǐng)勿將驗(yàn)證碼泄露給他人。",username,code);
}Spring事件最佳實(shí)踐
通用泛型類(lèi)事件
事件類(lèi)可以不繼承ApplicationEvent類(lèi),定義通用泛型類(lèi)事件對(duì)象,其他事件對(duì)象繼承該對(duì)象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GenericEvent<T> {
/**
* 業(yè)務(wù)類(lèi)型
*/
private String billType;
private T data;
}發(fā)送銀行成功事件
public class SendBankSuccessEvent extends GenericEvent<SendBank>{
public SendBankSuccessEvent(String billType, SendBank data) {
super(billType, data);
}
}發(fā)送銀行失敗事件
public class SendBankFailEvent extends GenericEvent<SendBank>{
public SendBankFailEvent(String billType, SendBank data) {
super(billType, data);
}
}發(fā)送銀行退回事件
public class SendBankGoBackEvent extends GenericEvent<Map>{
public SendBankGoBackEvent(String billType, Map data) {
super(billType, data);
}
}@Data
public class SendBank {
private String username;
private String paymentAccount;
private String money;
}
發(fā)布事件類(lèi)
發(fā)送事件類(lèi)一般可以直接是Service層的實(shí)現(xiàn)類(lèi)
@Service
public class SendBankServiceImpl {
@Resource
private ApplicationContext applicationContext;
public void sendBank(String event, String billType) {
SendBank sendBank = new SendBank();
sendBank.setUsername("風(fēng)雨修");
sendBank.setPaymentAccount("中國(guó)建設(shè)銀行");
sendBank.setMoney("2000");
if ("success".equals(event)) {
if ("DYZC".equals(billType)) {
applicationContext.publishEvent(new SendBankSuccessEvent("DYZC", sendBank));
}
if ("ZYSL".equals(billType)) {
applicationContext.publishEvent(new SendBankSuccessEvent("ZYSL", sendBank));
}
if ("SLHZ".equals(billType)) {
applicationContext.publishEvent(new SendBankSuccessEvent("SLHZ", sendBank));
}
} else if ("fail".equals(event)){
applicationContext.publishEvent(new SendBankFailEvent("ZYSL", sendBank));
} else if ("goBack".equals(event)){
HashMap<Object, Object> map = Maps.newHashMap();
map.put("username", "喜羊羊sk");
map.put("age", 23);
map.put("sex","男");
map.put("hobby","hello world");
applicationContext.publishEvent(new SendBankGoBackEvent("SLHZ", map));
}
}
}
事件監(jiān)聽(tīng)類(lèi)
@EventListener注解屬性
- classes: 此偵 聽(tīng)器處理的事件類(lèi)。
- condition:用于使事件處理成為條件的SpEL表達(dá)式,如果表達(dá)式的計(jì)算結(jié)果為布爾值true,則將處理該事件
一個(gè)事件可以有很多個(gè)監(jiān)聽(tīng)者,可以通過(guò)classes屬性指定具體監(jiān)聽(tīng)事件類(lèi),通過(guò)condition可以指定事件類(lèi)的屬性值滿(mǎn)足條件才生效執(zhí)行
@Component
@Slf4j
public class GenericEventListener {
/**
* 同一種事件,不同業(yè)務(wù)類(lèi)型。實(shí)現(xiàn)不同的監(jiān)聽(tīng)處理
* 通過(guò) @EventListener注解屬性
* classes: 此 偵聽(tīng)器處理的事件類(lèi)。
* condition:用于使事件處理成為條件的SpEL表達(dá)式,如果表達(dá)式的計(jì)算結(jié)果為布爾值true,則將處理該事件
*/
@EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType.equalsIgnoreCase('dyzc')")
public void DYZCsendBankSuccessListener(GenericEvent<SendBank> event) {
log.info("event: {}", event);
log.info("data: {}", event.getData());
}
@EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType.equalsIgnoreCase('zysl')")
public void ZYSLsendBankSuccessListener(GenericEvent<SendBank> event) {
log.info("event: {}", event);
log.info("data: {}", event.getData());
}
@EventListener(classes = SendBankSuccessEvent.class,condition = "#event.billType.equalsIgnoreCase('slhz')")
public void SLHZsendBankSuccessListener(GenericEvent<SendBank> event) {
log.info("event: {}", event);
log.info("data: {}", event.getData());
}
/**
* end
*
*/
@EventListener(classes = SendBankFailEvent.class)
public void sendBankFailListener(GenericEvent<SendBank> event) {
log.info("event: {}", event);
log.info("data: {}", event.getData());
}
@EventListener(classes = SendBankGoBackEvent.class)
public void sendGoBackListener(GenericEvent<Map> event) {
log.info("event: {}", event);
log.info("data: {}", event.getData());
}
}異步監(jiān)聽(tīng)處理
默認(rèn)情況下,發(fā)布事件和監(jiān)聽(tīng)事件都是同步執(zhí)行,在一個(gè)線程中的。在需要異步處理時(shí),可以在方法上加上@Async進(jìn)行異步化操作。此時(shí),可以定義一個(gè)線程池,同時(shí)開(kāi)啟異步功能,加入@EnableAsync。
@Async
@EventListener
public void sendEmail(String username) {
log.info("transactionEventListener start");
// 發(fā)生驗(yàn)證碼
send(username);
log.info("transactionEventListener finish");
}
事務(wù)綁定事件
當(dāng)一些場(chǎng)景下,比如在用戶(hù)注冊(cè)成功后,即數(shù)據(jù)庫(kù)事務(wù)提交了,之后再異步發(fā)送郵件等,不然會(huì)發(fā)生數(shù)據(jù)庫(kù)插入失敗,但事件卻發(fā)布了,也就是郵件發(fā)送成功了的情況。此時(shí),我們可以使用@TransactionalEventListener注解或者TransactionSynchronizationManager類(lèi)來(lái)解決此類(lèi)問(wèn)題,也就是:事務(wù)成功提交后,再執(zhí)行事件。
TransactionSynchronizationManager類(lèi)
@EventListener
public void afterRegisterSendMail(SendEmailEvent event) {
// Spring 4.2 之前
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
send(username);
}
}
);
}
上面的代碼將在事務(wù)提交后執(zhí)行.如果在非事務(wù)context中將拋出java.lang.IllegalStateException: Transaction synchronization is not active
@EventListener
public void afterRegisterSendMail(SendEmailEvent event) {
// Spring 4.2 之前
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
send(event);
}
});
} else {
send(username);
}
}這樣無(wú)論是否有事務(wù)都能兼容啦.
@TransactionalEventListener
Spring 4.2除了EventListener之外,額外提供了新的注解@TransactionalEventListener
@TransactionalEventListener
public void sendEmail(String username) {
log.info("transactionEventListener start");
// 發(fā)生驗(yàn)證碼
send(username);
log.info("transactionEventListener finish");
}
這個(gè)注解的強(qiáng)大之處在于可一直控制事務(wù)的 before/after commit, after rollback ,after completion (commit或 rollback). 默認(rèn)情況下,在事務(wù)中的Event將會(huì)被執(zhí)行,
到此這篇關(guān)于SpringBoot事件發(fā)布與監(jiān)聽(tīng)超詳細(xì)講解的文章就介紹到這了,更多相關(guān)SpringBoot事件發(fā)布與監(jiān)聽(tīng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 詳解SpringBoot實(shí)現(xiàn)ApplicationEvent事件的監(jiān)聽(tīng)與發(fā)布
- spring event 事件異步處理方式(發(fā)布,監(jiān)聽(tīng),異步處理)
- Spring事件發(fā)布監(jiān)聽(tīng),順序監(jiān)聽(tīng),異步監(jiān)聽(tīng)方式
- SpringBoot事件發(fā)布和監(jiān)聽(tīng)詳解
- 詳解Spring事件發(fā)布與監(jiān)聽(tīng)機(jī)制
- 解析Spring事件發(fā)布與監(jiān)聽(tīng)機(jī)制
- 詳解SpringBoot 發(fā)布ApplicationEventPublisher和監(jiān)聽(tīng)ApplicationEvent事件
- Spring的事件發(fā)布與監(jiān)聽(tīng)方式案例講解
相關(guān)文章
Spring?Boot?@Autowired?@Resource屬性賦值時(shí)機(jī)探究
這篇文章主要為大家介紹了Spring?Boot?@Autowired?@Resource屬性賦值時(shí)機(jī),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
SpringBoot使用WebSocket的方法實(shí)例詳解
WebSocket 是 HTML5 開(kāi)始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議。這篇文章主要介紹了SpringBoot使用WebSocket,需要的朋友可以參考下2019-06-06
OpenTelemetry初識(shí)及調(diào)用鏈Trace詳解
這篇文章主要為為大家介紹了OpenTelemetry初識(shí)及調(diào)用鏈Trace詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Java并發(fā)之原子性 有序性 可見(jiàn)性及Happen Before原則
一提到happens-before原則,就讓人有點(diǎn)“丈二和尚摸不著頭腦”。這個(gè)涵蓋了整個(gè)JMM中可見(jiàn)性原則的規(guī)則,究竟如何理解,把我個(gè)人一些理解記錄下來(lái)。下面可以和小編一起學(xué)習(xí)Java 并發(fā)四個(gè)原則2021-09-09
SpringBoot整合Gson 整合Fastjson的實(shí)例詳解
這篇文章主要介紹了SpringBoot整合Gson 整合Fastjson的實(shí)例詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
一篇文章教你如何用Java自定義一個(gè)參數(shù)校驗(yàn)器
這篇文章主要介紹了使用java自定義一個(gè)參數(shù)校驗(yàn)器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)2021-09-09
java實(shí)現(xiàn)發(fā)送email小案例
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)發(fā)送email小案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02
spring-data-jpa中findOne與getOne的區(qū)別說(shuō)明
這篇文章主要介紹了spring-data-jpa中findOne與getOne的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11

