springboot整合观察者,实现事件顺序自定义以及异步监听

1,702 阅读9分钟

前言

我个人接触到的第一个设计模式就是观察者模式。这是23种设计模式中最最常用的模式之一,非常重要。这篇我们就观察者模式聊一聊,原则上聊到哪里算哪里~

什么是观察者模式

基本定义:

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

  • 意图: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

  • 主要解决: 一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

  • 何时使用: 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

  • 如何解决: 使用面向对象技术,可以将这种依赖关系弱化。

  • 关键代码:在抽象类里有一个 ArrayList 存放观察者们。

  • 应用实例:  1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。

  • 优点:  1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。

  • 缺点:  1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

使用场景:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

注意事项:  1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

image.png

spring整合观察者模式

上面对于观察者模式的定义基本来自菜鸟教程,肯定看起来还是官方一些了, 这里我们简单地来说,观察者就是,当我在处理完一些业务之后,想有一些后续收尾工作。但这些工作我不想影响主流程,使其独立在主流程之外,此时观察者是非常合适的,举例说明下,例如在给用户办理业务成功之后想发送邮件或者短信给用户提醒下,那么此时就可以使用观察者。

反过来,在处理业务之前,对不同身份的用户(例如普通用户和会员用户)将执行不同流程的一套操作,此时什么设计模式看起来顺眼呢?没错---策略模式! 对不同场景选取合适的设计模式,会让应用层的编码来的简洁高效。

对于spring来说,整合观察者有大致类似的模板。下面我们直接动手。

  • 定义spring事件
  • 定义事件的监听方
  • 在需要的地方将事件进行推送

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.context.ApplicationEvent;

/**
 * 集成spring的事件即可。你可以在变量的位置盛放业务信息,这里仅仅以name和age进行模拟
 *
 * @author : wuwensheng
 * @date : 9:41 2021/11/29
 */
@Getter
@Setter
@ToString
public class TestEvent extends ApplicationEvent {
    private String name;

    private int age;

    private int id;

    public TestEvent(Object source, String name, int age, int id) {
        super(source);
        this.name = name;
        this.age = age;
        this.id = id;
    }
}

image.png

接下来马不停蹄地定义业务接口类,一个发送短信一个实现发送邮件。

/**
 * 发送短信的抽象类
 *
 * @author : wuwensheng
 * @date : 9:49 2021/11/29
 */
public interface SendSmsService {
    /**
     * 发送短信的方法,请传递用户的id
     *
     * @param id 用户id
     */
    public void sendSms(int id);
}
/**
 * 为用户发送邮件的类
 *
 * @author : wuwensheng
 * @date : 9:51 2021/11/29
 */
public interface SendEmailService {
    /**
     * 给用户发送邮件的方法,请传递用户id
     *
     * @param id 用户id
     */
    public void sendEmail(int id);
}

接下来实现这两个业务,由观察者完成这件事情的最终处理

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @author : wuwensheng
 * @date : 9:52 2021/11/29
 */
@Component
@Slf4j
public class SmsListener implements SendSmsService, ApplicationListener<TestEvent> {
    @Override
    public void sendSms(int id) {
        log.info("now send sms to id:{}", id);
    }

    @Override
    public void onApplicationEvent(TestEvent testEvent) {
        log.info("receive sms class receive testEvent:{}", testEvent);
        int id = testEvent.getId();
        sendSms(id);
    }
}
import com.cmdc.event.TestEvent;
import com.cmdc.service.SendEmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @author : wuwensheng
 * @date : 9:54 2021/11/29
 */
@Component
@Slf4j
public class EmailListener implements SendEmailService, ApplicationListener<TestEvent> {
    @Override
    public void sendEmail(int id) {
        log.info("now send email to id:{}", id);
    }

    @Override
    public void onApplicationEvent(TestEvent testEvent) {
        log.info("send email class receive testEvent:{}", testEvent);
        int id = testEvent.getId();
        sendEmail(id);
    }
}

实现这两个观察者的方式就是继承spring的ApplicationListener。要求去重写onApplicationEvent()方法!我们在onApplicationEvent()方法中去调用发短信或者发送邮件就可以了。

最终一步,在业务的合适位置发布此事件,使得观察者可以对发布的事件做出下一步的处理。我们就简单写个接口做个模拟就可以了,不整得特别麻烦。

import com.cmdc.event.TestEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试类
 *
 * @author : wuwensheng
 * @date : 10:00 2021/11/29
 */
@RestController
@Slf4j
public class TestController {

    @Autowired
    private ApplicationContext applicationContext;

    @GetMapping(value = "/test/event")
    public String testEvent() {
        TestEvent testEvent = new TestEvent(this, "小明", 20, 1);
        applicationContext.publishEvent(testEvent);
        return "successful";
    }
}

那么现在我们postman调用下接口看看会发生什么?

image.png ok,观察者成功接收到了事件并成功处理了事件!

解决事件的处理顺序问题

每次调用接口会发现,总是发送邮件随后才发送短信,这不太好,万一我就是想先发送短信随后才是邮件呢?下面给出改造的方案。 image.png

我们不再实现ApplicationListener,实现可以规定顺序的SmartApplicationListener!

package com.cmdc.listener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @author : wuwensheng
 * @date : 9:52 2021/11/29
 */
@Component
@Slf4j
public class SmsListener implements SendSmsService, SmartApplicationListener {
    @Override
    public void sendSms(int id) {
        log.info("now send sms to id:{}", id);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        TestEvent testEvent = (TestEvent) applicationEvent;
        log.info("receive sms class receive testEvent:{}", applicationEvent);
        int id = testEvent.getId();
        sendSms(id);
    }

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        return aClass == TestEvent.class;
    }

    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        return true;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
package com.cmdc.listener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendEmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @author : wuwensheng
 * @date : 9:54 2021/11/29
 */
@Component
@Slf4j
public class EmailListener implements SendEmailService, SmartApplicationListener {
    @Override
    public void sendEmail(int id) {
        log.info("now send email to id:{}", id);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        TestEvent testEvent = (TestEvent) applicationEvent;
        log.info("send email class receive testEvent:{}", testEvent);
        int id = testEvent.getId();
        sendEmail(id);
    }

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        return aClass == TestEvent.class;
    }

    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        return true;
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

注意getOrder方法,这个方法就是规定同个事件在不同观察者中的发生顺序的!由于EmailListener定义的值为1,SmsListener定义的值是0。所以发送短信应该先于发送邮件发生!我们使用postman测试结果也确实是如此的!

image.png

解决异步使用观察者的问题

如果我们某个观察者抛出异常了,其余观察者还能正常执行吗?我们试下,在SmsListener中故意写个异常,看看EmailListener还能否正常处理事件!

image.png

非常遗憾,后续的发送邮件的流程并未成功执行。这显然是我们不希望看到的,其实理想的状态下,我们希望在一个接口涉及到的一组业务中,可以启用不同的线程去执行这一组业务。它们独立工作,互相之间互不干扰。在同一时间并发地去执行。这在应用编程中是非常重要的。下面咱们着手解决这个问题。

image.png

在接口层和事件处理的地方都打印下线程名称,很容易就能发现,执行这些的都是同一个线程。下面我们尝试使得执行事件的线程可以来自咱们的线程池,使得监听者执行事件可以独立于主线程之外!

那么需要做的事情有三件事情

  • 实现一个自定义的线程池(不要使用Executors来创建,至于为什么小可爱们自行百度),自定义线程池这里不做演示,这个很简单。
  • 启动类增加一个@EnableAsync注解告诉全事件当前上下文中支持异步的使用
  • 在需要异步执行的方法上增加@Async注解

image.png

image.png

好了,现在再来调用下看下效果。

image.png

ok,两个事件都成功异步执行了。按照一开始预期的,使用咱们自定义的线程池中的线程执行了。这非常重要,是应用层并发编程必须掌握的手法,假设监听者中做处理的代码非常耗时,这里的意义将不言而喻。还有一件事情需要注意哈,使用异步之后设置的order执行顺序就失效了。这应该不难解释,异步本来就是让大家像脱缰的野马一样各自跑各自的,再有顺序就自相矛盾了。

拥抱注解---注解方式实现观察者

上面实现观察者使用的还是实现ApplicationListener或者实现SmartApplicationListener的方式,其实代码还是有些罗嗦的。下面咱们使用注解来达到一致的效果。

image.png

新建的类的代码粘贴在下面

package com.cmdc.listener.annotationlistener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendEmailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * @author : wuwensheng
 * @date : 14:51 2021/11/30
 */
@Component
@Slf4j
public class EmailAnnotationListener implements SendEmailService{

    @Async
    @Order(0)
    @EventListener(value = TestEvent.class)
    public void disposeEvent(TestEvent event){
        log.info("email ----- now running thread is:{}", Thread.currentThread().getName());
        log.info("send email class receive testEvent:{}", event);
        int id = event.getId();
        sendEmail(id);
    }

    @Override
    public void sendEmail(int id) {
        log.info("now send email to id:{}", id);
    }
}
package com.cmdc.listener.annotationlistener;

import com.cmdc.event.TestEvent;
import com.cmdc.service.SendSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * @author : wuwensheng
 * @date : 14:51 2021/11/30
 */
@Slf4j
@Component
public class SmsAnnotationListener implements SendSmsService {
    @Override
    public void sendSms(int id) {
        log.info("now send sms to id:{}", id);
    }

    @Async
    @Order(1)
    @EventListener(value = TestEvent.class)
    public void disposeEvent(TestEvent event){
        log.info("sms  ----- now running thread is:{}", Thread.currentThread().getName());
        log.info("send sms class receive testEvent:{}", event);
        int id = event.getId();
        sendSms(id);
    }
}

那么期望能成功调用咱们自己的线程池。发下请求看一下!

image.png

ok的,没什么问题。我们的整合也就整个地完成了。本篇涉及的一些概念都非常的好,没有了解过的同学一定要理解下!