写点什么

SpringBoot 事件监听机制及观察者 / 发布订阅模式详解

发布于: 2020 年 12 月 23 日
SpringBoot事件监听机制及观察者/发布订阅模式详解

本篇要点

  • 介绍观察者模式和发布订阅模式的区别。

  • 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@Slf4jpublic 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@Servicepublic 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@Slf4jpublic class EmailObserver implements Observer {
@Override public void update(String message) { log.info("向[{}]发送邮件", message); }}@Component@Slf4jpublic class CouponObserver implements Observer {
@Override public void update(String message) { log.info("向[{}]发送优惠券",message); }}
@Componentpublic 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 的对应方法,后者则主要使用消息队列异步实现。


尽管两者存在差异,但是他们其实在概念上相似,网上说法很多,不需要过于纠结,重点在于我们需要他们为什么出现,解决了什么问题。

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@Slf4jpublic class CouponService {    /**     * 监听用户注册事件,执行发放优惠券逻辑     */    @EventListener    public void addCoupon(UserRegisterEvent event) {        log.info("给用户[{}]发放优惠券", event.getUsername());    }}
复制代码

实现 ApplicationListener 的方式定义监听器

/** * 实现ApplicationListener<Event>的方式 * @author Summerday */@Service@Slf4jpublic class EmailService implements ApplicationListener<UserRegisterEvent> {    /**     * 监听用户注册事件, 异步发送执行发送邮件逻辑     */    @Override    @Async    public void onApplicationEvent(UserRegisterEvent event) {        log.info("给用户[{}]发送邮件", event.getUsername());    }}
复制代码

注册事件发布者

@Service@Slf4jpublic 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 // 开启异步@SpringBootApplicationpublic 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             : 给用户[天乔巴夏]发送邮件
复制代码

好啦,今天就分享到这里了!

原文链接:https://www.cnblogs.com/summerday152/p/14166152.html

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份 Java 核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!


发布于: 2020 年 12 月 23 日阅读数: 40
用户头像

领取资料添加小助理vx:bjmsb2020 2020.12.19 加入

Java领域;架构知识;面试心得;互联网行业最新资讯

评论 (1 条评论)

发布
用户头像
SpringBoot 事件监听机制及观察者 / 发布订阅模式详解
2020 年 12 月 23 日 15:10
回复
没有更多了
SpringBoot事件监听机制及观察者/发布订阅模式详解