SpringBoot事件監(jiān)聽機制及觀察者模式/發(fā)布訂閱模式
本篇要點
介紹觀察者模式和發(fā)布訂閱模式的區(qū)別。
SpringBoot快速入門事件監(jiān)聽。
什么是觀察者模式?
觀察者模式是經典行為型設計模式之一。
在GoF的《設計模式》中,觀察者模式的定義:在對象之間定義一個一對多的依賴,當一個對象狀態(tài)改變的時候,所有依賴的對象都會自動收到通知。如果你覺得比較抽象,接下來這個例子應該會讓你有所感覺:
就拿用戶注冊功能為例吧,假設用戶注冊成功之后,我們將會發(fā)送郵件,優(yōu)惠券等等操作,很容易就能寫出下面的邏輯:
@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);
// 發(fā)送郵件
emailService.sendEmail(username);
// 發(fā)送優(yōu)惠券
couponService.addCoupon(username);
return "注冊成功!";
}
}
這樣寫會有什么問題呢?受王爭老師啟發(fā):
方法調用時,同步阻塞導致響應變慢,需要異步非阻塞的解決方案。
注冊接口此時做的事情:注冊,發(fā)郵件,優(yōu)惠券,違反單一職責的原則。當然,如果后續(xù)沒有拓展和修改的需求,這樣子倒可以接受。
如果后續(xù)注冊的需求頻繁變更,相應就需要頻繁變更register方法,違反了開閉原則。
針對以上的問題,我們想一想解決的方案:
一、異步非阻塞的效果可以新開一個線程執(zhí)行耗時的發(fā)送郵件任務,但頻繁地創(chuàng)建和銷毀線程比較耗時,并且并發(fā)線程數無法控制,創(chuàng)建過多的線程會導致堆棧溢出。
二、使用線程池執(zhí)行任務解決上述問題。
@Service
@Slf4j
public class SimpleEmailService {
// 啟動一個線程執(zhí)行耗時操作
public void sendEmail(String username) {
Thread thread = new Thread(()->{
try {
// 模擬發(fā)郵件耗時操作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("給用戶 [{}] 發(fā)送郵件...", username);
});
thread.start();
}
}
@Slf4j
@Service
public class SimpleCouponService {
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 線程池執(zhí)行任務,減少資源消耗
public void addCoupon(String username) {
executorService.execute(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("給用戶 [{}] 發(fā)放優(yōu)惠券", username);
});
}
}
這里用戶注冊事件對【發(fā)送短信和優(yōu)惠券】其實是一對多的關系,可以使用觀察者模式進行解耦:
/**
* 主題接口
* @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("向[{}]發(fā)送郵件", message);
}
}
@Component
@Slf4j
public class CouponObserver implements Observer {
@Override
public void update(String message) {
log.info("向[{}]發(fā)送優(yōu)惠券",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";
}
}
發(fā)布訂閱模式是什么?
觀察者模式和發(fā)布訂閱模式是有一點點區(qū)別的,區(qū)別有以下幾點:
前者:觀察者訂閱主題,主題也維護觀察者的記錄,而后者:發(fā)布者和訂閱者不需要彼此了解,而是在消息隊列或代理的幫助下通信,實現松耦合。
前者主要以同步方式實現,即某個事件發(fā)生時,由Subject調用所有Observers的對應方法,后者則主要使用消息隊列異步實現。

盡管兩者存在差異,但是他們其實在概念上相似,網上說法很多,不需要過于糾結,重點在于我們需要他們?yōu)槭裁闯霈F,解決了什么問題。
Spring事件監(jiān)聽機制概述
SpringBoot中事件監(jiān)聽機制則通過發(fā)布-訂閱實現,主要包括以下三部分:
事件 ApplicationEvent,繼承JDK的EventObject,可自定義事件。
事件發(fā)布者 ApplicationEventPublisher,負責事件發(fā)布。
事件監(jiān)聽者 ApplicationListener,繼承JDK的EventListener,負責監(jiān)聽指定的事件。
我們通過SpringBoot的方式,能夠很容易實現事件監(jiān)聽,接下來我們改造一下上面的案例:
SpringBoot事件監(jiān)聽
定義注冊事件
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定義監(jiān)聽器
/**
* 注解方式 @EventListener
* @author Summerday
*/
@Service
@Slf4j
public class CouponService {
/**
* 監(jiān)聽用戶注冊事件,執(zhí)行發(fā)放優(yōu)惠券邏輯
*/
@EventListener
public void addCoupon(UserRegisterEvent event) {
log.info("給用戶[{}]發(fā)放優(yōu)惠券", event.getUsername());
}
}
實現ApplicationListener的方式定義監(jiān)聽器
/**
* 實現ApplicationListener<Event>的方式
* @author Summerday
*/
@Service
@Slf4j
public class EmailService implements ApplicationListener<UserRegisterEvent> {
/**
* 監(jiān)聽用戶注冊事件, 異步發(fā)送執(zhí)行發(fā)送郵件邏輯
*/
@Override
@Async
public void onApplicationEvent(UserRegisterEvent event) {
log.info("給用戶[{}]發(fā)送郵件", event.getUsername());
}
}
注冊事件發(fā)布者
@Service
@Slf4j
public class UserService implements ApplicationEventPublisherAware {
// 注入事件發(fā)布者
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* 發(fā)布事件
*/
public void register(String username) {
log.info("執(zhí)行用戶[{}]的注冊邏輯", 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=Java爛豬皮,結果如下:
2020-12-21 00:59:46.679 INFO 12800 --- [nio-8081-exec-1] com.hyh.service.UserService : 執(zhí)行用戶[Java爛豬皮]的注冊邏輯
2020-12-21 00:59:46.681 INFO 12800 --- [nio-8081-exec-1] com.hyh.service.CouponService : 給用戶[Java爛豬皮]發(fā)放優(yōu)惠券
2020-12-21 00:59:46.689 INFO 12800 --- [task-1] com.hyh.service.EmailService : 給用戶[Java爛豬皮]發(fā)送郵件
剩下的就不會給大家一展出來了,以上資料按照一下操作即可獲得
——將文章進行轉發(fā)和評論,關注公眾號【Java烤豬皮】,關注后繼續(xù)后臺回復領取口令“ 666 ”即可免費領文章取中所提供的資料。

騰訊、阿里、滴滴后臺試題匯集總結 — (含答案)
面試:史上最全多線程序面試題!
最新阿里內推Java后端試題
JVM難學?那是因為你沒有真正看完整這篇文章

關注作者微信公眾號 — 《JAVA烤豬皮》
了解了更多java后端架構知識以及最新面試寶典
看完本文記得給作者點贊+在看哦~~~大家的支持,是作者來源不斷出文的動力~
