参考:SpringBoot使用ApplicationEvent&Listener完成业务解耦
参考:Spring Boot 解耦之事件驱动
一、前言
1、1 使用场景
日常开发中,常见的比如用户注册账号操作,当用户注册完毕之后,可能还需要处理以下一些事情:
- 发送确认邮件
- 赠送用户积分、成长值
- 赠送优惠券
问题:
- 假如以上所有的操作全部都耦合在一个service业务处理代码中,后续操作一直没有完成,那么用户是不是要长时间等待
- 如果邮件服务器挂了,注册还能成功吗
- 后期维护起来相关代码也会非常麻烦,甚至会出现一些漏洞等
从上述例子可以看出,当用户注册完毕之后,发布一个命令给第三方的观察者,观察者接收到相关命令之后,就可以来处理之后的相关事件,那么程序就可以解耦各个环节的依赖关系,这就是事件驱动模型,内部实现原理是观察者设计模式。
1、2 事件驱动定义
事件驱动模型也就是我们常说的观察者,或者发布-订阅模型;理解它的几个关键点:
- 首先是一种对象间的一对多的关系;最简单的如交通信号灯,信号灯是目标(一方),行人注视着信号灯(多方);
- 当目标发送改变(发布),观察者(订阅者)就可以接收到改变;
- 观察者如何处理(如行人如何走,是快走/慢走/不走,目标不会管的),目标无需干涉;所以就松散耦合了它们之间的关系。
spring主要是通过ApplicationEvent
以及Listener
为我们提供事件监听、订阅等相关事件处理。
二、项目中应用
2、1 搭建springboot项目
略。
2、2 定义事件
/**
* 自定义订单监听事件,继承了ApplicationEvent,并重载构造函数
* <p>
* 构造函数的参数可以任意指定,其中source参数指的是发生事件的对象,而第二个参数是我们自定义的注册事件对象,该对象可以在监听内被获取。
*/
@Getter
public class OrderCancelEvent extends ApplicationEvent {
// 注入订单业务对象
private OmsOrder omsOrder;
public OrderCancelEvent(Object source, OmsOrder omsOrder) {
super(source);
this.omsOrder = omsOrder;
}
}
事件是事件驱动的核心,上述OrderCancelEvent
就是自定义的一个事件,继承了ApplicationEvent
,并重写了其构造函数,第一个参数一般我们在发布事件时使用的是this
关键字代替本类对象,而第二个参数则根据具体业务具体定义,主要就是为了使监听器可以监听到相关事件。
2、3 创建事件监听器
事件监听器的创建方式有好多种,eg:
@EventListener
注解、实现ApplicationListener
泛型接口、实现SmartApplicationListener
接口等,我们下面来讲解下这三种方式分别如何实现。
@EventListener
使用该注解是最简单的一种方式,只需在方法上加上此注解即可。
/**
* 用途:消息事件监听器
* 作者: jingwenhao
* 时间: 2019/7/26 15:29
*/
@Component
@Slf4j
public class MsgSendListener {
@Autowired
private IImsNotificationService notificationService;
/**
* 支付成功之后,异步发送消息事件
*
* @param event 消息事件
*/
@EventListener
@Async
public void sendMsgAfterPaySucc(MsgOrderPayEvent event) {
try {
log.debug("——发送消息事件开始执行——");
Thread.sleep(3000);// 休息3秒
// 获取事件实际对象
Map<String, Object> eventMap = event.getEventMap();
// 具体的业务逻辑方法
notificationService.sendMsgAfterPaySucc(eventMap);
log.debug("——发送消息事件结束——");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 超时订单关闭,异步发送消息事件
*
* @param event 消息事件
*/
@EventListener
@Async
public void sendMsgAfterAutoCancel(MsgOrderCancelEvent event) {
try {
log.debug("——发送消息事件开始执行——");
Thread.sleep(3000);// 休息3秒
Map<String, Object> eventMap = event.getEventMap();
notificationService.sendMsgAfterAutoCancel(eventMap);
log.debug("——发送消息事件结束——");
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 实现ApplicationListener泛型接口
@Component
public class RegisterListener implements ApplicationListener<UserRegisterEvent>
{
/**
* 实现监听
* @param userRegisterEvent
*/
@Override
public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
//获取注册用户对象
UserBean user = userRegisterEvent.getUser();
//../省略逻辑
//输出注册用户信息
System.out.println("注册信息,用户名:"+user.getName()+",密码:"+user.getPassword());
}
}
这里直接复制了博客上的写法,这种写法主要是实现了ApplicationListener接口,并将事先定义好的事件作为泛型对象传递了过去,UserRegisterEvent事件发布时监听程序会自动调用onApplicationEvent方法并且将UserRegisterEvent对象作为参数传递。
- 实现SmartApplicationListener
/**
* 订单超时自动关闭监听任务
*/
@Component
@Slf4j
public class OrderAutoCloseListener implements SmartApplicationListener {
@Autowired
private IOmsOrderService orderService;//注入订单业务接口
/**
* 该方法返回true&supportsSourceType同样返回true时,才会调用该监听内的onApplicationEvent方法
*
* @param aClass 接收到的监听事件类型
* @return
*/
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
//只有OrderEvent监听类型才会执行下面逻辑
return aClass == OrderCancelEvent.class;
}
/**
* 该方法返回true&supportsEventType同样返回true时,才会调用该监听内的onApplicationEvent方法
*
* @param aClass
* @return
*/
@Override
public boolean supportsSourceType(Class<?> aClass) {
//只有在OmsOrderServiceImpl内发布的UserRegisterEvent事件时才会执行下面逻辑
return aClass == OmsOrderServiceImpl.class;
}
/**
* supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
*
* @param applicationEvent 具体监听实例,这里是orderEvent
*/
@Override
@Async
public void onApplicationEvent(ApplicationEvent applicationEvent) {
log.debug("————订单超时关闭事件开始准备,倒计时1分钟");
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
//转换事件类型
OrderCancelEvent orderCancelEvent = (OrderCancelEvent) applicationEvent;
//获取订单对象
OmsOrder order = orderCancelEvent.getOmsOrder();
Runnable payTimeoutTask = new Runnable() {
@Override
public void run() {
//执行订单超时,系统取消订单操作
orderService.cancelOrderAutoById(order.getId());
}
};
executor.schedule(payTimeoutTask, 2, TimeUnit.MINUTES);
try {
//每分钟检查任务是否完成,完成后关闭任务线程
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
log.debug("————订单超时关闭事件执行结束");
}
public void SyncAndAsync() throws ExecutionException, InterruptedException {
AsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
System.out.println("First");
//Future<String> future = executor.submit(new CallableTask("Second")); //同步
executor.execute(new RunnableTask("go!"));
//System.out.println(future.get());
System.out.println("Third");
}
class RunnableTask implements Runnable{
private String parameter;
public RunnableTask(String parameter) {
super();
this.parameter = parameter;
}
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
System.out.println(parameter+ "Second");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class CallableTask implements Callable<String> {
private String parameter;
public CallableTask(String parameter) {
super();
this.parameter = parameter;
}
@Override
public String call() throws Exception {
Thread.sleep(5 * 1000);
return parameter+ " finished!";
}
}
/**
* 同步情况下监听执行的顺序
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
SmartApplicationListener接口继承了全局监听ApplicationListener,并且泛型对象使用的ApplicationEvent来作为全局监听,可以理解为使用SmartApplicationListener作为监听父接口的实现,监听所有事件发布。
既然是监听所有的事件发布,那么SmartApplicationListener接口添加了两个方法supportsEventType、supportsSourceType来作为区分是否是我们监听的事件,只有这两个方法同时返回true时才会执行onApplicationEvent方法。
可以看到除了上面的方法,还提供了一个getOrder方法,这个方法就可以解决执行监听的顺序问题,return的数值越小证明优先级越高,执行顺序越靠前。
2、4 使用@Async实现异步监听
@Aysnc其实是Spring内的一个组件,可以完成对类内单个或者多个方法实现异步调用,这样可以大大的节省等待耗时。内部实现机制是线程池任务ThreadPoolTaskExecutor
,通过线程池来对配置@Async的方法或者类做出执行动作。
2、5 具体业务场景
- 订单创建时,发布
超时未支付自动关闭
事件:
@Service
public class OrderService
{
@Autowired
private ApplicationContext applicationContext;
/**
* 生成订单
* @param order 订单对象
*/
public void generateOrder(OmsOrder order)
{
//....省略逻辑
//发布订单超时关闭事件
applicationContext.publishEvent(new OrderCancelEvent(this, order));
}
}
- 成功付款时,发布
推送消息
事件:
@Service
public class OrderService
{
@Autowired
private ApplicationContext applicationContext;
/**
* 支付订单
* @param order 订单对象
*/
public void pay(OmsOrder order)
{
//....省略逻辑
// 发送支付成功消息事件
Map<String, Object> eventMap = Maps.newHashMap();
eventMap.put("order", order);
applicationContext.publishEvent(new MsgOrderPayEvent(this, eventMap));
}
}
2.6 拓展
上述2.5中描述了一个场景:订单创建时,发布超时未支付自动关闭
事件,在这里,介绍几种电商项目中如何处理超时未支付自动关闭订单的方案:
- 第一种:数据库加两个字段, 一个字段标记: 是否付款,一个字段标记:过期时间,查询时去判断是否 付款和超时,然后更新状态。但是这种方案导致
占用商品资源
。 - 第二种:定时任务一直扫描,扫描到满足条件的就进行更新操作。但是这种方案导致
不能准确处理订单状态
。 - 第三种:TODO 依赖于第三方框架,比如框架Quartz、rabbitMQ等,不太懂,就不解释了哈。
我目前是这样处理超时未支付自动关闭:当订单创建完成时,发布超时未支付自动关闭
事件,同时系统中还有一个每1分钟执行一次的扫描超时未支付订单
的定时任务,两种方案结合目前是可以解决上述问题。采用异步事件
和定时任务
结合的方案,有以下好处:
- 更加准确的处理超时未支付订单,及时释放库存
- 防止系统宕机导致的进程丢失,原有的事件任务无法执行,通过定时任务可以有效解决此问题
三、小结
使用事件驱动模型可以大大降低我们实际项目中的代码耦合,降低了前后端交互的响应耗时,而且还减少了后期业务变更引起的代码调整的难度。具体使用步骤如下:
- 定义事件
- 创建事件监听器
- 业务代码中发布事件