Spring事件监听机制

发布时间:2023年12月19日
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

发短信案例问题分析

假设现在有这么一个业务场景:

用户在京西商城下单成功后,平台要发送短信通知用户下单成功

我们最直观的想法是直接在order()方法中添加发送短信的业务代码:

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

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

咋一看没什么不妥,但随着时间推移,上面的代码就会暴露出局限性:

一个月后,京西搞了自建物流体系,用户下单成功后,还需要通知物流系统发货

于是你又要打开OrderService修改order()方法:

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

嗯,完美。

又过了一个月,东哥被抓了,股价暴跌,决定卖掉自己的车队,所以下单后就不用通知车队了

重新修改OrderService:

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

又过了一个月,东哥明尼苏达州荣耀归来:回来做我的兄弟一起开车吧。

好的,东哥。

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

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

就在这时候,组长拉住了你,语重心长地和你说:

小伙子,知道什么叫“以增量的方式应对变化的需求”吗?听过Spring事件监听机制吗?

说时迟那时快,组长拿来一支笔和一张纸,唰唰唰画了一张图:

你顿时心领神会,扑通一声跪在了地上,开始敲起了代码。

利用Spring事件机制完成需求

环境准备

还是用之前的SpringBoot项目即可,只要你引入了spring-boot-starter-web。

<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 {

    /**
     * Create a new ApplicationEvent.
     *
     * @param source the object on which the event initially occurred (never {@code null})
     */
    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("发送短信...");
    }
}

Test

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test {

    @Autowired
    private OrderService orderService;

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

输出:

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

流程示意图(为什么SmsService能监听到?更详细的流程会在山寨版Spring事件监听机制中介绍):

如果后期针对下单成功有新的操作,可以新写一个事件监听类:

/**
 * 物流服务
 */
@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事件机制的设计初衷。人家会想不到你要搞异步通知?

当SimpleApplicationEventMulticaster中的Executor不为null,就会执行异步通知。

@Configuration
public class AsyncEventConfig {

    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster
                = new SimpleApplicationEventMulticaster();

        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }

}

把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事件机制哪怕使用了异步,本质是还是一种方法调用,宕机了就没了。

细节

之前在知乎,有知友留言,为什么OrderService发布事件要放入this,感觉没什么用:

大家有注意到这个细节吗~你们觉得有什么用呢?

拓展阅读:spring 事件监听同时支持同步事件及异步事件

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬
文章来源:https://blog.csdn.net/smart_an/article/details/134987208
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。