本篇要点

  • 介绍观察者模式和发布订阅模式的区别。
  • 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的整合哦。

参考阅读

SpringBoot事件监听机制及观察者模式/发布订阅模式的更多相关文章

  1. SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)

    SpringBoot中文注释项目Github地址: https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE 本篇接 SpringApplicat ...

  2. Spring 事件监听机制及原理分析

    简介 在JAVA体系中,有支持实现事件监听机制,在Spring 中也专门提供了一套事件机制的接口,方便我们实现.比如我们可以实现当用户注册后,给他发送一封邮件告诉他注册成功的一些信息,比如用户订阅的主 ...

  3. Spring ApplicationContext(八)事件监听机制

    Spring ApplicationContext(八)事件监听机制 本节则重点关注的是 Spring 的事件监听机制,主要是第 8 步:多播器注册:第 10 步:事件注册. public void ...

  4. 7_3.springboot2.x启动配置原理_3.事件监听机制

    事件监听机制配置在META-INF/spring.factories ApplicationContextInitializer SpringApplicationRunListenerioc容器中的 ...

  5. Java事件监听机制与观察者设计模式

    一. Java事件监听机制 1. 事件监听三要素: 事件源,事件对象,事件监听器 2. 三要素之间的关系:事件源注册事件监听器后,当事件源上发生某个动作时,事件源就会调用事件监听的一个方法,并将事件对 ...

  6. Halo 开源项目学习(六):事件监听机制

    基本介绍 Halo 项目中,当用户或博主执行某些操作时,服务器会发布相应的事件,例如博主登录管理员后台时发布 "日志记录" 事件,用户浏览文章时发布 "访问文章" ...

  7. 4.JAVA之GUI编程事件监听机制

    事件监听机制的特点: 1.事件源 2.事件 3.监听器 4.事件处理 事件源:就是awt包或者swing包中的那些图形用户界面组件.(如:按钮) 事件:每一个事件源都有自己特点有的对应事件和共性事件. ...

  8. .NET事件监听机制的局限与扩展

    .NET中把“事件”看作一个基本的编程概念,并提供了非常优美的语法支持,对比如下C#和Java代码可以看出两种语言设计思想之间的差异. // C#someButton.Click += OnSomeB ...

  9. 关于事件监听机制的总结(Listener和Adapter)

    记得以前看过事件监听机制背后也是有一种设计模式的.(设计模式的名字记不清了,只记得背后实现的数据结构是数组.) 附上事件监听机制的分析图: 一个事件源可以承载多个事件(只要这个事件源支持这个事件就可以 ...

随机推荐

  1. react-hash-calendar,移动端日期时间选择插件

    按照惯例,先上效果图 vue 版本同款日历:https://github.com/TangSY/vue-hash-calendar react-hash-calendar 支持手势滑动操作 上下滑动 ...

  2. Linux中配置环境变量

    Linux中环境变量的搭建(推荐用法) 第一步:进入到/etc/profile.d文件夹下 cd /etc/profile.d 第二步:创建并编辑一个my_env.sh文件 vim my_env.sh ...

  3. 洛谷 P1284 三角形牧场 题解(背包+海伦公式)

    题目链接 题目大意 给你 n块木板(n<=40),每块木板长度为\(l[i]<=40\) 每块木板都要用,求最大的三角形面积×100,答案直接舍去小数 题目思路 首先如果已知三条边的长度可 ...

  4. CentOS 7定时执行python脚本

    CentOS 7定时执行python脚本 在CentOS下,可以使用crontab进行定时任务的处理. 一.crontab的安装 默认情况下,CentOS 7中已经安装有crontab,如果没有安装, ...

  5. 19_B门长时曝光APP

    知识很基础-- 前几天买了个单反,特别想拍B门长时间曝光的效果.后来想想不如自己写个APP,实现屏幕背景的随机颜色以及全屏显示文字. 先上图: 这两张图片的左侧都很亮,这是因为APP里面忘记把&quo ...

  6. Beta冲刺随笔——Day_Seven

    这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 Beta 冲刺 这个作业的目标 团队进行Beta冲刺 作业正文 正文 其他参考文献 无 今日事今日毕 林涛: ...

  7. 强大的拉姆表达式转Sql 类库 - SqlSugar 隐藏功能之Lambda

    使用场景 1.Lambda to sql 一直是ORM中最难的功能之一,如果有现成的解析库那么自已写一个ORM难度将大大降低 2.通过Lambda作为KEY进行缓存操作,特别是仓储模式想要拿到表达式进 ...

  8. 老猿学5G扫盲贴:中国移动5G融合计费漫游计费架构和路由方案

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一. ...

  9. 转:解析HTTP协议六种请求方法,get,head,put,delete,post有什么区别

    解析HTTP协议六种请求方法,get,head,put,delete,post有什么区别 标准Http协议支持六种请求方法,即: 1.GET 2.POST 3.PUT 4.Delete 5.HEAD ...

  10. 第9.11节 Python中IO模块文件打开读写操作实例

    为了对前面学习的内容进行一个系统化的应用,老猿写了一个程序来进行文件相关操作功能的测试. 一. 测试程序说明 该程序允许测试人员选择一个文件,自己输入文件打开模式.写入文件的位置以及写入内容,程序按照 ...