本篇要點
- 介紹觀察者模式和釋出訂閱模式的區別。
- SpringBoot快速入門事件監聽。
什麼是觀察者模式?
觀察者模式是經典行為型設計模式之一。
在GoF的《設計模式》中,觀察者模式的定義:在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會自動收到通知。如果你覺得比較抽象,接下來這個例子應該會讓你有所感覺:
就拿使用者註冊功能為例吧,假設使用者註冊成功之後,我們將會傳送郵件,優惠券等等操作,很容易就能寫出下面的邏輯:
@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方法,違反了開閉原則。
針對以上的問題,我們想一想解決的方案:
一、非同步非阻塞的效果可以新開一個執行緒執行耗時的傳送郵件任務,但頻繁地建立和銷燬執行緒比較耗時,並且併發執行緒數無法控制,建立過多的執行緒會導致堆疊溢位。
二、使用執行緒池執行任務解決上述問題。
@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);
});
}
}
這裡使用者註冊事件對【傳送簡訊和優惠券】其實是一對多的關係,可以使用觀察者模式進行解耦:
/**
* 主題介面
* @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<Observer> 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
儘管兩者存在差異,但是他們其實在概念上相似,網上說法很多,不需要過於糾結,重點在於我們需要他們為什麼出現,解決了什麼問題。
Spring事件監聽機制概述
SpringBoot中事件監聽機制則通過釋出-訂閱實現,主要包括以下三部分:
- 事件 ApplicationEvent,繼承JDK的EventObject,可自定義事件。
- 事件釋出者 ApplicationEventPublisher,負責事件釋出。
- 事件監聽者 ApplicationListener,繼承JDK的EventListener,負責監聽指定的事件。
我們通過SpringBoot的方式,能夠很容易實現事件監聽,接下來我們改造一下上面的案例:
SpringBoot事件監聽
定義註冊事件
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定義監聽器
/**
* 註解方式 @EventListener
* @author Summerday
*/
@Service
@Slf4j
public class CouponService {
/**
* 監聽使用者註冊事件,執行發放優惠券邏輯
*/
@EventListener
public void addCoupon(UserRegisterEvent event) {
log.info("給使用者[{}]發放優惠券", event.getUsername());
}
}
實現ApplicationListener的方式定義監聽器
/**
* 實現ApplicationListener<Event>的方式
* @author Summerday
*/
@Service
@Slf4j
public class EmailService implements ApplicationListener<UserRegisterEvent> {
/**
* 監聽使用者註冊事件, 非同步傳送執行傳送郵件邏輯
*/
@Override
@Async
public void onApplicationEvent(UserRegisterEvent event) {
log.info("給使用者[{}]傳送郵件", event.getUsername());
}
}
註冊事件釋出者
@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));
}
}
定義介面
@RestController
@RequestMapping("/event")
public class UserEventController {
@Autowired
private UserService userService;
@GetMapping("/register")
public String register(String username){
userService.register(username);
return "恭喜註冊成功!";
}
}
主程式類
@EnableAsync // 開啟非同步
@SpringBootApplication
public class SpringBootEventListenerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootEventListenerApplication.class, args);
}
}
測試介面
啟動程式,訪問介面:http://localhost:8081/event/register?username=天喬巴夏
,結果如下:
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,另有其他SpringBoot的整合哦。