<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
比如: 我們現在的應用是Maven聚合工程的多模組方式開發。現在有biz模組、bill模組、pay模組。biz模組有一個使用者註冊功能,bill模組有一個發生郵件的功能,每次使用者註冊都需要呼叫bill模組的傳送郵件功能。我們只需要在pom引入bill模組的依賴,注入bill模組傳送郵件的service介面,即可以完成操作。
現在業務有調整,pay模組也需要完成支付業務傳送郵件,也需要和上面操作一樣,操作會很費時費力。有沒有一種辦法可以把它們解耦出來?
答案是有的,我們可以使用事件的方式,biz模組只要註冊成功,釋出一個使用者註冊成功的事件,pay模組支付成功,釋出一個支付成功的事件。讓bill模組監聽了此事件的去做剩下的傳送郵件事件就好了。對於事件釋出者而言,不需要關心誰監聽了該事件,以此來解耦業務。注意:必須在同一個Spring容器內,不能完成跨容器的通訊,跨容器的通訊可以考慮中介軟體訊息佇列
Spring的事件應該是在3.x版本就釋出的功能了,並越來越完善,其為bean和bean之間的訊息通訊提供了支援。
ApplicationEvent以及Listener是Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是為了系統業務邏輯之間的解耦,提高可延伸性以及可維護性。
其執行的流程大致為:
當一個事件源產生事件時,它通過事件釋出器ApplicationEventPublisher釋出事件,然後事件廣播器ApplicationEventMulticaster會去事件登入檔ApplicationContext中找到事件監聽器ApplicationListnener,並且逐個執行監聽器的onApplicationEvent方法,從而完成事件監聽器的邏輯。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Spring中,事件源不強迫繼承ApplicationEvent
介面的,也就是可以直接釋出任意一個物件類(實體類,Map,List,String等任意物件類)。但內部其實是使用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 + '}'; } }
這裡我手動處理的事務,也可以使用@Transactional
處理。這裡我主要模擬釋出事件發生異常和監聽事件中發生異常的情況。
預設情況下,釋出事件和監聽事件都是在一個執行緒中同步執行的。業務釋出事件類中,當try程式碼塊int a = 1 / 0;
模擬儲存使用者異常,會進入到catch塊中回滾儲存事務,不會發布事件。
當業務類儲存使用者,並執行事件釋出邏輯成功,使用@EventListener
的監聽類中發生異常,也會到釋出事件的catch塊中回滾儲存的事務。即證明了釋出事件和監聽事件都是在一個執行緒中同步執行的。事務是一致性的。
@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); // 模擬異常回滾 // int a = 1 / 0; } // 1、釋出ApplicationEvent物件 eventPublisher.publishEvent(new SendEmailEvent(this,user)); // 2、傳送郵件 (釋出任意一個物件類) eventPublisher.publishEvent(user.getUsername()); log.info("[UserService] finish insert saveUser"); // 手動提交事務 end transactionManager.commit(transaction); // 手動提交事務 start } // 異常回滾 catch (Exception e) { log.error("saveUser異常 fail:{}", e); // 手動回滾事務 start transactionManager.rollback(transaction); // 手動回滾事務 end } return result; } }
@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); // 可能由於網路頻寬,傳送驗證碼異常,模擬監聽事件異常 int i = 1/0; log.info("【{}】本次登陸驗證碼為:{},請在5分鐘內完成驗證,請勿將驗證碼洩露給他人。",username,code); } }
使用@EventListener的condition可以實現更加精細的事件監聽,condition支援SpEL表示式,可根據事件源的引數來判斷是否監聽。事件源為物件,可以使用物件.屬性。如: @EventListener(condition = "#customEvent.billType=='pay'")
@EventListener(condition = "#username.equals('風雨')") public void sendEmail(String username) { log.info("transactionEventListener start"); // 發生驗證碼 send(username); log.info("transactionEventListener finish"); } public void send(String username) { String code = UUID.randomUUID().toString().replace("_", "").substring(0, 4); log.info("生成驗證碼: {}",code); // 可能由於網路頻寬,傳送驗證碼異常,模擬監聽事件異常 int i = 1/0; log.info("【{}】本次登陸驗證碼為:{},請在5分鐘內完成驗證,請勿將驗證碼洩露給他人。",username,code); }
事件類可以不繼承ApplicationEvent類,定義通用泛型類事件物件,其他事件物件繼承該物件
@Data @AllArgsConstructor @NoArgsConstructor public class GenericEvent<T> { /** * 業務型別 */ private String billType; private T data; }
傳送銀行成功事件
public class SendBankSuccessEvent extends GenericEvent<SendBank>{ public SendBankSuccessEvent(String billType, SendBank data) { super(billType, data); } }
傳送銀行失敗事件
public class SendBankFailEvent extends GenericEvent<SendBank>{ public SendBankFailEvent(String billType, SendBank data) { super(billType, data); } }
傳送銀行退回事件
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; }
傳送事件類一般可以直接是Service層的實現類
@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)); } } }
@EventListener註解屬性
一個事件可以有很多個監聽者,可以通過classes屬性指定具體監聽事件類,通過condition可以指定事件類的屬性值滿足條件才生效執行
@Component @Slf4j public class GenericEventListener { /** * 同一種事件,不同業務型別。實現不同的監聽處理 * 通過 @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()); } }
預設情況下,釋出事件和監聽事件都是同步執行,在一個執行緒中的。在需要非同步處理時,可以在方法上加上@Async進行非同步化操作。此時,可以定義一個執行緒池,同時開啟非同步功能,加入@EnableAsync。
@Async @EventListener public void sendEmail(String username) { log.info("transactionEventListener start"); // 發生驗證碼 send(username); log.info("transactionEventListener finish"); }
當一些場景下,比如在使用者註冊成功後,即資料庫事務提交了,之後再非同步傳送郵件等,不然會發生資料庫插入失敗,但事件卻釋出了,也就是郵件傳送成功了的情況。此時,我們可以使用@TransactionalEventListener
註解或者TransactionSynchronizationManager
類來解決此類問題,也就是:事務成功提交後,再執行事件。
@EventListener public void afterRegisterSendMail(SendEmailEvent event) { // Spring 4.2 之前 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { send(username); } } ); }
上面的程式碼將在事務提交後執行.如果在非事務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); } }
這樣無論是否有事務都能相容啦.
Spring 4.2除了EventListener之外,額外提供了新的註解@TransactionalEventListener
@TransactionalEventListener public void sendEmail(String username) { log.info("transactionEventListener start"); // 發生驗證碼 send(username); log.info("transactionEventListener finish"); }
這個註解的強大之處在於可一直控制事務的 before/after commit, after rollback ,after completion (commit或 rollback)
. 預設情況下,在事務中的Event將會被執行,
到此這篇關於SpringBoot事件釋出與監聽超詳細講解的文章就介紹到這了,更多相關SpringBoot事件釋出與監聽內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45