Spring Boot 解耦之事件驱动

参考: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分钟执行一次的扫描超时未支付订单的定时任务,两种方案结合目前是可以解决上述问题。采用异步事件定时任务结合的方案,有以下好处:

  • 更加准确的处理超时未支付订单,及时释放库存
  • 防止系统宕机导致的进程丢失,原有的事件任务无法执行,通过定时任务可以有效解决此问题

三、小结

使用事件驱动模型可以大大降低我们实际项目中的代码耦合,降低了前后端交互的响应耗时,而且还减少了后期业务变更引起的代码调整的难度。具体使用步骤如下:

  • 定义事件
  • 创建事件监听器
  • 业务代码中发布事件
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容