1. 程式人生 > >SpringBoot事件監聽機制及觀察者模式/釋出訂閱模式

SpringBoot事件監聽機制及觀察者模式/釋出訂閱模式

[toc] ## 本篇要點 - 介紹觀察者模式和釋出訂閱模式的區別。 - SpringBoot快速入門事件監聽。 ## 什麼是觀察者模式? 觀察者模式是經典行為型設計模式之一。 在GoF的《設計模式》中,觀察者模式的定義:**在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會自動收到通知**。如果你覺得比較抽象,接下來這個例子應該會讓你有所感覺: 就拿使用者註冊功能為例吧,假設使用者註冊成功之後,我們將會發送郵件,優惠券等等操作,很容易就能寫出下面的邏輯: ```java @RestController @RequestMapping("/user") public class SimpleUserController { @Autowired private SimpleEmailService emailService; @Autowired private SimpleCouponService couponService; @Autowired private SimpleUserService userService; @GetMapping("/register") public String register(String username) { // 註冊 userService.register(username); // 傳送郵件 emailService.sendEmail(username); // 傳送優惠券 couponService.addCoupon(username); return "註冊成功!"; } } ``` 這樣寫會有什麼問題呢?受王爭老師啟發: - 方法呼叫時,同步阻塞導致響應變慢,需要非同步非阻塞的解決方案。 - 註冊介面此時做的事情:註冊,發郵件,優惠券,違反單一職責的原則。當然,如果後續沒有拓展和修改的需求,這樣子倒可以接受。 - 如果後續註冊的需求頻繁變更,相應就需要頻繁變更register方法,違反了開閉原則。 針對以上的問題,我們想一想解決的方案: 一、非同步非阻塞的效果可以新開一個執行緒執行耗時的傳送郵件任務,但頻繁地建立和銷燬執行緒比較耗時,並且併發執行緒數無法控制,建立過多的執行緒會導致堆疊溢位。 二、使用執行緒池執行任務解決上述問題。 ```java @Service @Slf4j public class SimpleEmailService { // 啟動一個執行緒執行耗時操作 public void sendEmail(String username) { Thread thread = new Thread(()->{ try { // 模擬發郵件耗時操作 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("給使用者 [{}] 傳送郵件...", username); }); thread.start(); } } @Slf4j @Service public class SimpleCouponService { ExecutorService executorService = Executors.newSingleThreadExecutor(); // 執行緒池執行任務,減少資源消耗 public void addCoupon(String username) { executorService.execute(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("給使用者 [{}] 發放優惠券", username); }); } } ``` 這裡使用者註冊事件對【傳送簡訊和優惠券】其實是一對多的關係,可以使用觀察者模式進行解耦: ```java /** * 主題介面 * @author Summerday */ public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(String message); } /** * 觀察者介面 * @author Summerday */ public interface Observer { void update(String message); } @Component @Slf4j public class EmailObserver implements Observer { @Override public void update(String message) { log.info("向[{}]傳送郵件", message); } } @Component @Slf4j public class CouponObserver implements Observer { @Override public void update(String message) { log.info("向[{}]傳送優惠券",message); } } @Component public class UserRegisterSubject implements Subject { @Autowired List observers; @Override public void registerObserver(Observer observer) { observers.add(observer); } @Override public void removeObserver(Observer observer) { observers.remove(observer); } @Override public void notifyObservers(String username) { for (Observer observer : observers) { observer.update(username); } } } @RestController @RequestMapping("/") public class UserController { @Autowired UserRegisterSubject subject; @Autowired private SimpleUserService userService; @GetMapping("/reg") public String reg(String username) { userService.register(username); subject.notifyObservers(username); return "success"; } } ``` ## 釋出訂閱模式是什麼? 觀察者模式和釋出訂閱模式是有一點點區別的,區別有以下幾點: - 前者:觀察者訂閱主題,主題也維護觀察者的記錄,而後者:**釋出者和訂閱者不需要彼此瞭解**,而是在訊息佇列或代理的幫助下通訊,實現鬆耦合。 - 前者主要以同步方式實現,即某個事件發生時,由Subject呼叫所有Observers的對應方法,後者則主要使用訊息佇列非同步實現。 > 圖源:https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c ![](https://img2020.cnblogs.com/blog/1771072/202012/1771072-20201221010037493-1393298351.jpg) 儘管兩者存在差異,但是他們其實在概念上相似,網上說法很多,不需要過於糾結,重點在於我們需要他們為什麼出現,解決了什麼問題。 ## Spring事件監聽機制概述 SpringBoot中事件監聽機制則通過釋出-訂閱實現,主要包括以下三部分: - 事件 ApplicationEvent,繼承JDK的EventObject,可自定義事件。 - 事件釋出者 ApplicationEventPublisher,負責事件釋出。 - 事件監聽者 ApplicationListener,繼承JDK的EventListener,負責監聽指定的事件。 我們通過SpringBoot的方式,能夠很容易實現事件監聽,接下來我們改造一下上面的案例: ## SpringBoot事件監聽 ### 定義註冊事件 ```java public class UserRegisterEvent extends ApplicationEvent { private String username; public UserRegisterEvent(Object source) { super(source); } public UserRegisterEvent(Object source, String username) { super(source); this.username = username; } public String getUsername() { return username; } } ``` ### 註解方式 @EventListener定義監聽器 ```java /** * 註解方式 @EventListener * @author Summerday */ @Service @Slf4j public class CouponService { /** * 監聽使用者註冊事件,執行發放優惠券邏輯 */ @EventListener public void addCoupon(UserRegisterEvent event) { log.info("給使用者[{}]發放優惠券", event.getUsername()); } } ``` ### 實現ApplicationListener的方式定義監聽器 ```java /** * 實現ApplicationListener的方式 * @author Summerday */ @Service @Slf4j public class EmailService implements ApplicationListener { /** * 監聽使用者註冊事件, 非同步傳送執行傳送郵件邏輯 */ @Override @Async public void onApplicationEvent(UserRegisterEvent event) { log.info("給使用者[{}]傳送郵件", event.getUsername()); } } ``` ### 註冊事件釋出者 ```java @Service @Slf4j public class UserService implements ApplicationEventPublisherAware { // 注入事件釋出者 private ApplicationEventPublisher applicationEventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } /** * 釋出事件 */ public void register(String username) { log.info("執行使用者[{}]的註冊邏輯", username); applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username)); } } ``` ### 定義介面 ```java @RestController @RequestMapping("/event") public class UserEventController { @Autowired private UserService userService; @GetMapping("/register") public String register(String username){ userService.register(username); return "恭喜註冊成功!"; } } ``` ### 主程式類 ```java @EnableAsync // 開啟非同步 @SpringBootApplication public class SpringBootEventListenerApplication { public static void main(String[] args) { SpringApplication.run(SpringBootEventListenerApplication.class, args); } } ``` ### 測試介面 啟動程式,訪問介面:`http://localhost:8081/event/register?username=天喬巴夏`,結果如下: ```java 2020-12-21 00:59:46.679 INFO 12800 --- [nio-8081-exec-1] com.hyh.service.UserService : 執行使用者[天喬巴夏]的註冊邏輯 2020-12-21 00:59:46.681 INFO 12800 --- [nio-8081-exec-1] com.hyh.service.CouponService : 給使用者[天喬巴夏]發放優惠券 2020-12-21 00:59:46.689 INFO 12800 --- [ task-1] com.hyh.service.EmailService : 給使用者[天喬巴夏]傳送郵件 ``` ## 原始碼下載 本文內容均為對優秀部落格及官方文件總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的程式碼樣例已經全部上傳至Gitee:[https://gitee.com/tqbx/springboot-samples-learn](https://gitee.com/tqbx/springboot-samples-learn),另有其他SpringBoot的整合哦。 ## 參考閱讀 - [芋道 Spring Boot 事件機制 Event 入門](http://www.iocoder.cn/Spring-Boot/Event/) - [觀察者模式和釋出訂閱模式有什麼不同?](https://www.zhihu.com/question/23486749) - [觀察者模式](https://www.runoob.com/design-pattern/observer-pattern.html) - [設計模式之美:觀察者模式](https://time.geekbang.org/column/article/210170) - [Observer vs Pub-Sub pattern](https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b2