Spring Boot事件监听,这东西到底解决啥问题?

一、先聊个场景,你就明白这玩意儿干啥的了

点过外卖吧?那咱们就用这个场景来说事。

你掏出手机下了单,付了钱。接下来会发生啥?

  • 厨房那头开始备菜炒菜(这件事耽误不得,客户饿着呢)
  • 手机收到一条短信:"您的订单已收到,预计30分钟送达"
  • 你的会员账户里多了一堆积分
  • 店长那个收银小喇叭"叮咚"响了一声

现在你设想一下,如果这家店的流程必须先发完短信 → 再加完积分 → 再响铃 → 最后厨房才能开始炒菜,你会不会觉得这店脑子有问题?

万一短信网关卡了三秒,后厨那师傅就拿着锅铲干等着。你作为客户,等餐时间硬生生多了好几秒。

稍微正常点的店,肯定是后厨该干啥干啥,短信、积分、响铃这几件事各自并行处理,谁也别耽误谁。

Spring Boot的事件监听机制,解决的就是这类问题------核心业务只管宣布"出事了",至于谁要响应、怎么响应,核心业务一概不关心。 大家各干各的,谁也不等谁。


二、就三个角色,没你想的那么复杂

这套东西其实就三个参与者:

  • 事件:就是一个装数据的包裹,告诉你"刚才发生了一件事,这是相关数据"
  • 发布者:就是"喊一嗓子"的那个人,通常是你核心业务代码里的某个Service
  • 监听器:就是"听到动静就干活"的那个人,可能有多个,各干各的活

三、别废话了,直接上代码

老规矩,用一个真实的订单创建场景,从最基本的写法开始,一步一步往里加东西。

3.1 先加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

就这俩,别的不用。

3.2 第一步:把事件定义出来(就是那个"包裹")

订单创建之后,我们需要把订单号、用户ID、金额这几个关键数据打包传出去,让监听器们拿着用。

java 复制代码
package com.example.demo.event;

import org.springframework.context.ApplicationEvent;

/**
 * 订单创建事件
 * 说白了就是一个装数据的筐,谁监听这个事件,谁就能从这个筐里拿数据
 */
public class OrderCreatedEvent extends ApplicationEvent {

    /**
     * 订单号
     */
    private final String orderNo;

    /**
     * 用户ID
     */
    private final Long userId;

    /**
     * 订单金额(单位:分,避免浮点数精度问题)
     */
    private final Long amount;

    /**
     * 构造方法
     * @param source 谁发的这个事件,通常传this就行
     */
    public OrderCreatedEvent(Object source, String orderNo, Long userId, Long amount) {
        super(source);  // 父类要求,传进去就完了
        this.orderNo = orderNo;
        this.userId = userId;
        this.amount = amount;
    }

    // -------- Getter 让监听器能拿到数据 --------
    public String getOrderNo() {
        return orderNo;
    }

    public Long getUserId() {
        return userId;
    }

    public Long getAmount() {
        return amount;
    }

    @Override
    public String toString() {
        return "OrderCreatedEvent{" +
                "orderNo='" + orderNo + '\'' +
                ", userId=" + userId +
                ", amount=" + amount +
                '}';
    }
}

说一嘴:那个source是父类ApplicationEvent的构造参数,大多数情况下你传this就行了。真正干活用到的是我们自己加的那三个字段。

3.3 第二步:写监听器------"收到包裹后我要干啥"

监听器的工作就是"当某某事件发生了,我该做什么"。

java 复制代码
package com.example.demo.listener;

import com.example.demo.event.OrderCreatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * 订单事件监听器
 */
@Component
public class OrderEventListener {

    private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);

    /**
     * 发短信
     * Spring看到@EventListener注解的方法,并且参数是OrderCreatedEvent,
     * 就会在事件发布时自动调用这个方法
     */
    @EventListener
    public void handleSendSms(OrderCreatedEvent event) {
        log.info("【短信】给用户 {} 发短信,订单号:{}", event.getUserId(), event.getOrderNo());
        
        // 假装调了一下短信接口,sleep模拟网络IO
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        log.info("【短信】发送成功");
    }

    /**
     * 加积分
     */
    @EventListener
    public void handleAddPoints(OrderCreatedEvent event) {
        log.info("【积分】用户 {} 获得 {} 积分", event.getUserId(), event.getAmount() / 100);
        // 实际项目里这里会调积分服务的接口
    }

    /**
     * 写日志
     */
    @EventListener
    public void handleWriteLog(OrderCreatedEvent event) {
        log.info("【日志】记录订单创建:{}", event.getOrderNo());
    }
}

这里有个细节:一个类里可以写多个@EventListener方法,每个方法监听同一个事件,各干各的事,互不影响。你也可以把它们拆到不同的类里,看你的组织习惯。

3.4 第三步:发布事件------"喊一嗓子"

在订单Service里,把ApplicationEventPublisher注入进来,然后在合适的位置把事件发出去。

java 复制代码
package com.example.demo.service;

import com.example.demo.event.OrderCreatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Service
public class OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    @Autowired
    private ApplicationEventPublisher eventPublisher;  // 就靠这个发布事件

    @Transactional
    public void createOrder(Long userId, Long amount) {
        // ---------- 核心:保存订单 ----------
        String orderNo = "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6);
        log.info("【核心】订单创建:{},用户:{},金额:{}分", orderNo, userId, amount);

        // 实际项目里这里会调用 orderMapper.insert(order)
        // 如果插入失败,事务回滚,后续代码不会执行

        // ---------- 发布事件:通知别人"订单好了" ----------
        // 注意:此时事务还没提交,监听器们正在同步执行
        log.info("【核心】发布事件...");
        OrderCreatedEvent event = new OrderCreatedEvent(this, orderNo, userId, amount);
        eventPublisher.publishEvent(event);
        log.info("【核心】事件发布完毕");

        // 方法结束,事务提交
    }
}

3.5 第四步:搞个Controller触发一下

java 复制代码
package com.example.demo.controller;

import com.example.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/order/create")
    public Map<String, Object> createOrder(
            @RequestParam(defaultValue = "1001") Long userId,
            @RequestParam(defaultValue = "10000") Long amount) {

        long start = System.currentTimeMillis();
        orderService.createOrder(userId, amount);
        long cost = System.currentTimeMillis() - start;

        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "订单创建成功");
        result.put("costTime", cost + "ms");
        return result;
    }
}

3.6 跑一下看看

启动项目,POST请求一下 /order/create,控制台大概输出这样:

复制代码
【核心】订单创建:ORD1693567890123abcde,用户:1001,金额:10000分
【核心】发布事件...
【短信】给用户 1001 发短信,订单号:ORD1693567890123abcde
【短信】发送成功
【积分】用户 1001 获得 100 积分
【日志】记录订单创建:ORD1693567890123abcde
【核心】事件发布完毕

注意看顺序 :所有监听器执行完了,才打印"事件发布完毕"。说明默认是同步的,一个一个排队执行。

还有一个坑:要是任何一个监听器报错了,整个事务会回滚,订单创建失败。这点后面会说怎么处理。

四、你肯定会问:跟直接调@Async有啥区别?

这问题问到点子上了。很多人第一反应就是"我用@Async注解也能异步执行啊,搞这么复杂干嘛?"

4.1 先看直接用@Async的方式

java 复制代码
@Service
public class OrderServiceV1 {

    @Autowired
    private SmsService smsService;
    @Autowired
    private PointsService pointsService;

    @Transactional
    public void createOrder(Long userId, Long amount) {
        String orderNo = saveOrder(userId, amount);
        
        // 显式调用,明确告诉程序要做什么
        smsService.sendSmsAsync(orderNo, userId);
        pointsService.addPointsAsync(userId, amount);
    }
}

这么写有啥毛病?最大的毛病就是------如果产品经理说"再加个App推送",你得改OrderServiceV1这个类的代码。改一次两次还好,改个十次八次这个类就变成个上千行的怪物了。

4.2 再看事件的方式

java 复制代码
@Service
public class OrderServiceV2 {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Transactional
    public void createOrder(Long userId, Long amount) {
        String orderNo = saveOrder(userId, amount);
        
        // 只发事件,谁处理、怎么处理,一概不管
        publisher.publishEvent(new OrderCreatedEvent(this, orderNo, userId, amount));
    }
}

区别在哪? 主方法里只有一个publishEvent,新增功能完全不用动这个类,新建一个监听器就完事了。

@Async直接调 事件监听
主方法知道谁在干活吗? 知道,挨个调了一遍 不知道,只管发事件
新增一个后续动作 改主方法代码 新建监听器就行
代码耦合度 越来越高 主类始终保持干净

说白了就是:你点了一个外卖,你是直接打电话给厨师、骑手、店长一个一个通知,还是在外卖App上点一下"下单"按钮完事?事件监听就是后者。

五、进阶:事务提交后再执行,这个很实用

上面那个版本有个隐藏问题,你可能已经注意到了:

java 复制代码
@Transactional
public void createOrder(Long userId, Long amount) {
    orderMapper.insert(order);      // ① 插入数据,但事务还没提交
    publisher.publishEvent(event);  // ② 发布事件,监听器开始干活
    // ③ 方法结束,事务提交
}

监听器在步骤②执行的时候,数据库事务还没提交。如果监听器里需要查询刚插入的订单数据,查不到!因为别的线程根本看不到未提交的数据。

这个在真实项目里经常遇到,解决方案就是Spring提供的@TransactionalEventListener

5.1 四个事务阶段

Spring提供了四个"钩子",让你精确控制监听器在事务的哪个阶段执行:

阶段 执行时机 啥时候用?
BEFORE_COMMIT 事务提交前 需要和主业务在同一事务里,失败了要一起回滚
AFTER_COMMIT 事务提交后 最常用,确保数据落盘了再做别的
AFTER_ROLLBACK 事务回滚后 记录失败原因、做补偿
AFTER_COMPLETION 事务完成后(不管成功还是失败) 清理临时资源

5.2 用代码说话

java 复制代码
package com.example.demo.listener;

import com.example.demo.event.OrderCreatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class OrderTransactionListener {

    private static final Logger log = LoggerFactory.getLogger(OrderTransactionListener.class);

    /**
     * 事务提交后再执行------这个最常用
     * 只有主方法的事务成功提交了,这个方法才会被触发
     * 如果主方法回滚了,这个方法不会执行
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleAfterCommit(OrderCreatedEvent event) {
        log.info("【事务提交后】数据已经落盘了,订单号:{},放心查吧", event.getOrderNo());
        // 这里适合干这些事:发MQ消息、调第三方接口、同步ES......
    }

    /**
     * 事务回滚后执行
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleAfterRollback(OrderCreatedEvent event) {
        log.info("【事务回滚后】订单创建失败了,订单号:{}", event.getOrderNo());
        // 记录失败日志、触发补偿流程
    }
}

跑一下,看输出顺序:

复制代码
【核心】订单创建:ORD1693567890123
【核心】发布事件...
【核心】事件发布完毕
(事务提交中...)
【事务提交后】数据已经落盘了,订单号:ORD1693567890123,放心查吧

注意:AFTER_COMMIT的监听器是在方法结束、事务提交之后才执行的。这时候数据已经妥妥地写在数据库里了,监听器里怎么查都安全。

5.3 生产级写法长这样

真实项目里通常是"事务提交后 + 异步执行"的组合:

java 复制代码
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleSendSms(OrderCreatedEvent event) {
    try {
        // 调用短信网关
        smsClient.send(event.getUserId(), "订单" + event.getOrderNo() + "已创建");
    } catch (Exception e) {
        // 注意:千万不能把异常抛出去,否则会影响其他监听器
        // 记录日志 + 报警,让人工介入
        log.error("短信发送失败,订单号:{}", event.getOrderNo(), e);
        // 可以存到失败表,定时任务重试
    }
}

注意别忘了在启动类上加@EnableAsync,不然@Async不生效。

六、几个容易踩的坑

坑1:监听器抛异常,主业务回滚

默认情况下,监听器和主业务在同一个事务里。监听器报错,主业务也跟着回滚。

java 复制代码
// ❌ 这么写有风险
@EventListener
public void handle(OrderCreatedEvent event) {
    smsService.send(event.getUserId());  // 万一发短信报错,整个订单都没了
}
java 复制代码
// ✅ 稳妥的做法
@EventListener
public void handle(OrderCreatedEvent event) {
    try {
        smsService.send(event.getUserId());
    } catch (Exception e) {
        log.error("短信发送失败,不影响主流程", e);
        // 记下来,后面人工或定时任务补偿
    }
}

坑2:监听器太慢,拖垮了整个接口响应时间

默认同步执行,如果监听器里有个耗时操作,接口响应时间就会被拖长。

java 复制代码
// ❌ 同步执行,接口被卡住
@EventListener
public void handle(OrderCreatedEvent event) {
    Thread.sleep(5000);  // 用户要等5秒才能看到"下单成功"
}
java 复制代码
// ✅ 加个@Async就解决了
@Async
@EventListener
public void handle(OrderCreatedEvent event) {
    Thread.sleep(5000);  // 接口瞬间返回,监听器在后台慢慢跑
}

坑3:多个监听器的执行顺序不确定

如果你依赖某个监听器先执行,另一个后执行,需要加@Order明确顺序:

java 复制代码
@Component
@Order(1)
public class FirstListener {
    @EventListener
    public void handle(OrderCreatedEvent event) {
        // 先执行
    }
}

@Component
@Order(2)
public class SecondListener {
    @EventListener
    public void handle(OrderCreatedEvent event) {
        // 后执行
    }
}

七、项目结构长这样

复制代码
src/main/java/com/example/demo/
├── DemoApplication.java                  # 启动类,记得加@EnableAsync
├── controller/
│   └── OrderController.java              # 入口
├── service/
│   └── OrderService.java                 # 核心业务,发布事件
├── event/
│   └── OrderCreatedEvent.java            # 事件定义
└── listener/
    ├── OrderEventListener.java           # 普通监听器
    └── OrderTransactionListener.java     # 事务监听器

八、总结一下

回到最开头的那个外卖场景。用事件监听的方式去组织代码,核心业务只管"下单"这一件事,短信、积分、日志都是旁边各自独立的监听器,谁出问题也不影响核心流程。新增一个"给店长发通知"的功能,也只需要加一个新的监听器,不用动核心代码。

记住两句话就够了:

  1. 核心业务只管喊一嗓子,谁响应、怎么响应,跟它没关系。
  2. 跟数据一致性相关的监听器,用@TransactionalEventListener(phase = AFTER_COMMIT)确保事务提交后再执行,同时用@Async异步处理,别让旁路逻辑拖慢主流程。

代码这东西,你自己敲一遍比看十遍都管用。建议把上面的代码复制到你自己的项目里跑一下,改改参数,看看日志输出顺序,感受一下事件机制的运作方式。遇到问题欢迎留言交流。

相关推荐
ttod_qzstudio8 个月前
备忘录之事件监听器绑定陷阱:为什么 .bind(this) 会移除失败?
javascript·typescript·内存泄漏·事件监听
学Java的小半1 年前
用键盘实现控制小球上下移动——java的事件控制
java·开发语言·算法·intellij-idea·gui·事件监听
molashaonian2 年前
Spring事件监听机制详解
java·后端·spring·观察者模式·event·事件监听
哗哗的世界2 年前
技术派Spring事件监听机制及原理
spring boot·spring·事件监听