Spring 事件监听机制


假设现在有这么一个业务场景:用户在某购物软件下单成功后,平台要发送短信通知用户下单成功。我们最直观的想法是直接在 order() 方法中添加发送短信的业务代码:

public void order(){
  // 下单操作
  log.info("下单成功, 订单号:{}", orderNumber);
  // 发送短信
  sendSms();
}

public void sendSms(){
    // ... 
}

这样做没什么不妥,但是随着时间推移,上面的代码就会暴露出局限性:

一个月后,该商城搞了自建物流体系,用户下单成功后,还需要通知物流系统发货,于是你又要打开 OrderService 修改 order() 方法:

public void order(){
  // 下单成功
  log.info("下单成功, 订单号:{}", orderNumber);
  // 发送短信
  sendSms();
  // 通知车队发货 
  notifyCar();
}

嗯,nice。

又过了一段时间,老板被抓了,股价暴跌,于是老板决定卖掉自己的车队,所以下单后就不用通知车队了。

重新修改 OrderService:

public void order(){
  // 下单成功
  log.info("下单成功, 订单号:{}", orderNumber);
  // 发送短信
  sendSms();
  // 车队没了,注释掉这行代码 
  // notifyCar();
}

又过了一段时间,老板荣耀归来,东山再起,又把车队重现组建了起来。

public void order(){
  // 下单成功
  log.info("下单成功, 订单号:{}", orderNumber);
  // 发送短信
  sendSms();
  // 车队买回来了,放开这段代码
  notifyCar()
}

车队回来了,你却受不了这大起大落异常刺激的生活,决定离职。

就在这时候,组长拉住了你,语重心长地和你说:小伙子,知道什么叫 “以增量的方式应对变化的需求” 吗?听过Spring事件监听机制吗?

说时迟那时快,组长拿来一支笔和一张纸,唰唰唰画了一张图:
在这里插入图片描述

利用Spring事件机制完成需求

环境准备

创建一个 SpringBoot 项目即可。
在这里插入图片描述

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

代码

OrderService

/**
 * 订单服务
 */
@Service
public class OrderService {

    @Autowired
    private ApplicationContext applicationContext;

    public void order() {
        // 下单成功
        System.out.println("下单成功...");
        // 发布通知(传入了当前对象)
        applicationContext.publishEvent(new OrderSuccessEvent(this));
        System.out.println("main线程结束...");
    }
}

OrderSuccessEvent(继承ApplicationEvent,自定义事件)

public class OrderSuccessEvent extends ApplicationEvent {

    public OrderSuccessEvent(Object source) {
        super(source);
    }
}

SmsService(实现ApplicationListener,监听OrderSuccessEvent)

/**
 * 短信服务,监听OrderSuccessEvent
 */
@Service
public class SmsService implements ApplicationListener<OrderSuccessEvent> {

    @Override
    public void onApplicationEvent(OrderSuccessEvent event) {
        this.sendSms();
    }

    /**
     * 发送短信
     */
    public void sendSms() {
        System.out.println("发送短信...");
    }
}

SpringEventApplicationTests

@RunWith(SpringRunner.class)
@SpringBootTest
class SpringEventApplicationTests {

    @Autowired
    private OrderService orderService;

    @Test
    public void testSpringEvent() {
        orderService.order();
    }

}

输出

下单成功...
发送短信...
main线程结束...

流程示意图
在这里插入图片描述
如果后期针对下单成功有新的操作,可以新写一个事件监听类:

/**
 * 物流服务
 */
@Service
public class CarService  implements ApplicationListener<OrderSuccessEvent> {
    @Override
    public void onApplicationEvent(OrderSuccessEvent event) {
        this.dispatch();
    }

    public void dispatch() {
        System.out.println("发车咯...");
    }
}

在这里插入图片描述
这就是以增量的方式应对变化的需求,而不是去修改已有的代码(ServiceA)。同样的,对老项目改造时也是如此,如果你不知道原来的接口是干嘛的,最好不要去动它,宁愿新写一个接口,即一般提倡“对扩展开放,对修改关闭”。

上面 SmsService 既是一个服务,还是一个 Listener,因为它既有 @Service 又实现了 ApplicationListener 接口。

但是仅仅是为了一个监听回调方法而实现一个接口,未免麻烦,所以 Spring 提供了注解的方式:

/**
 * 短信服务,监听OrderSuccessEvent,但不用实现ApplicationListener
 */
@Service
public class SmsService {

    /**
     * 发送短信 @EventListener指定监听的事件
     */
    @EventListener(OrderSuccessEvent.class)
    public void sendSms() {

        try {
            Thread.sleep(1000L * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("发送短信...");
    }

}

Spring发布异步事件

看似很完美了,但是你注意到 Spring 默认的事件机制是同步的:
在这里插入图片描述如果针对OrderService 下单成功的操作越来越多,比如下单后需要完成的对应操作有十几个,那么等十几个其他服务都调用完毕,时间会很长。
在这里插入图片描述
所以,你必须想办法把 Spring 的事件机制改成异步的,尽可能快地返回下单的结果本身,而不是等其他附属服务全部完成(涉及到其他问题暂时按下不表)。

要想把 Spring 事件机制改造成异步通知,最粗暴的方法是:
OrderService

/**
 * 订单服务
 */
@Service
public class OrderService {

    @Autowired
    private ApplicationContext applicationContext;

    public void order() {
        // 下单成功
        System.out.println("下单成功...");
        // 发布通知
        new Thread(() ->{
            applicationContext.publishEvent(new OrderSuccessEvent(this));
        }).start();
        System.out.println("main线程结束...");
        // 等SmsService结束
        try {
            Thread.sleep(1000L * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

SmsService

/**
 * 短信服务,监听OrderSuccessEvent
 */
@Service
public class SmsService implements ApplicationListener<OrderSuccessEvent> {

    @Override
    public void onApplicationEvent(OrderSuccessEvent event) {
        this.sendSms();
    }

    /**
     * 发送短信
     */
    public void sendSms() {
        try {
            Thread.sleep(1000L * 3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送短信...");
    }
}

输出

下单成功...
main线程结束...
发送短信.

在这里插入图片描述
当然,这种做法其实违背了 Spring 事件机制的设计初衷。人家会想不到你要搞异步通知?

把OrderService改回来:

@Service
public class OrderService {

    @Autowired
    private ApplicationContext applicationContext;

    public void order() {
        // 下单成功
        System.out.println("下单成功...");
        // 发布通知
        applicationContext.publishEvent(new OrderSuccessEvent(this));
        System.out.println("main线程结束...");
        // 等SmsService结束
        try {
            Thread.sleep(1000L * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此时仍然是异步的。

Spring 事件机制适合单体应用,同一个 JVM 且并发不大的情况,如果是分布式应用,推荐使用MQ。

Spring 事件监听机制和 MQ 有相似的地方,也有不同的地方。MQ 允许跨 JVM,因为它本身是独立于项目之外的,切提供了消息持久化的特性,而 Spring 事件机制哪怕使用了异步,本质是还是一种方法调用,宕机了就没了。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuZhan7710

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值