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