5621 字
约 18 分钟
0
Spring Event 入门:用事件发布订阅实现业务解耦

Spring Event 入门:用事件发布订阅实现业务解耦

在实际项目中,我们经常会遇到这样的业务:

用户注册成功
    ↓
保存用户
    ↓
发送欢迎邮件
    ↓
初始化积分
    ↓
发送注册通知
    ↓
记录操作日志

刚开始业务比较简单的时候,我们可能直接全部写在一个 Service 方法里:

public void register(User user) {

    // 保存用户
    userRepository.save(user);

    // 发送邮件
    emailService.sendWelcomeEmail(user);

    // 初始化积分
    pointsService.initPoints(user.getId());

    // 发送通知
    notificationService.send(user);

    // 记录日志
    logService.save(user);
}

代码看起来好像没什么问题。

但是随着业务越来越复杂,这种写法的问题会越来越明显。

UserService 本来应该只关心“用户注册”,现在却同时依赖:

EmailService
PointsService
NotificationService
LogService
...

以后再增加一个“注册后赠送优惠券”的需求,还得继续修改:

register()

最终可能变成:

register()
    ├── 保存用户
    ├── 发邮件
    ├── 初始化积分
    ├── 发优惠券
    ├── 发消息
    ├── 记录日志
    ├── 数据统计
    └── ...

这就是典型的业务耦合越来越严重

而 Spring Event,就是解决这类问题的一种非常轻量的方式。


一、Spring Event 是什么?

Spring Event 是 Spring 提供的一套应用内事件发布订阅机制

它的设计思想其实很简单:

发布者
   ↓
发布 Event
   ↓
Spring
   ↓
找到监听这个 Event 的 Listener
   ↓
执行 Listener

例如用户注册完成以后,不再直接调用:

emailService.send();
pointsService.init();
couponService.send();

而是只告诉系统:

“用户已经注册成功了”

也就是发布一个:

UserRegisteredEvent

至于注册之后需要干什么,UserService 不关心。

可能有:

邮件模块
积分模块
优惠券模块
日志模块
统计模块

它们各自监听:

UserRegisteredEvent

然后处理自己的事情。

这实际上就是经典的观察者模式。Spring 官方也将 ApplicationContext 的事件机制描述为观察者模式的一种实现。现代 Spring 还允许直接发布普通 Java 对象,不要求事件必须继承 ApplicationEvent。(Home )


二、Spring Event 到底是怎么实现解耦的?

理解 Spring Event 最关键的地方,其实不是会写:

@EventListener

而是理解:

它到底解除了什么耦合?

继续看之前的代码。

原来是:

public void register(User user) {

    userRepository.save(user);

    emailService.sendWelcomeEmail(user);

    pointsService.initPoints(user.getId());

    couponService.sendCoupon(user.getId());
}

这里的依赖关系是:

UserService
   │
   ├── EmailService
   ├── PointsService
   └── CouponService

UserService 必须认识所有业务模块。

如果以后增加:

RegisterStatisticsService

你还得修改:

UserService

于是业务之间产生了直接依赖。

使用 Spring Event 后:

UserService
     ↓
UserRegisteredEvent
     ↓
Spring Event
   ↙   ↓   ↘
邮件  积分  优惠券

UserService 只负责:

publisher.publishEvent(event);

它甚至不知道有没有:

EmailListener
PointsListener
CouponListener

以后新增一个:

StatisticsListener

原来的:

UserService

一行代码都不用修改。

这就是所谓的:

发布者和订阅者解耦。

真正发生变化的是依赖关系。

原来:

UserService
    ↓
EmailService
    ↓
PointsService
    ↓
CouponService

现在:

UserService
    ↓
UserRegisteredEvent

其他模块则依赖这个事件:

UserRegisteredEvent
    ↑
EmailListener

UserRegisteredEvent
    ↑
PointsListener

UserRegisteredEvent
    ↑
CouponListener

于是:

“调用某个具体业务”

变成了:

“声明某件事情已经发生”。

这是事件驱动设计里面非常重要的思想。


三、Spring Event 的三个核心角色

学习 Spring Event,只需要先理解三个东西:

Event
Publisher
Listener

Event 表示:

发生了什么事情

Publisher 表示:

谁把这个事情发布出去

Listener 表示:

谁关心这个事情

整个过程就是:

Publisher
    ↓
 Event
    ↓
Listener

接下来直接写代码。


四、第一步:定义事件

假设我们的业务是:

用户注册成功

定义一个事件:

public record UserRegisteredEvent(
        Long userId,
        String email
) {
}

如果不用 Java record,普通 Java 类同样可以:

@Data
@AllArgsConstructor
public class UserRegisteredEvent {

    private Long userId;

    private String email;
}

这里不需要:

extends ApplicationEvent

Spring 从 4.2 开始支持直接发布任意普通对象;如果发布的不是 ApplicationEvent,Spring 会在内部将它包装成 PayloadApplicationEvent。(Home )

事件本身最好不要写大量业务逻辑。

它更像一个:

“消息载体”

负责告诉监听器:

发生了什么
+
处理这个事件需要哪些数据

例如:

UserRegisteredEvent

userId = 1001
email = hello@example.com

五、第二步:发布事件

Spring 提供了:

ApplicationEventPublisher

专门负责发布事件。

例如:

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    private final ApplicationEventPublisher eventPublisher;

    public void register(User user) {

        // 核心业务
        userRepository.save(user);

        // 发布“用户注册成功”事件
        eventPublisher.publishEvent(
                new UserRegisteredEvent(
                        user.getId(),
                        user.getEmail()
                )
        );
    }
}

现在 UserService 做的事情就非常简单:

保存用户
   ↓
告诉系统:
用户注册成功了

至于后面发生什么,它不管。

这时候代码已经开始解耦了。


六、第三步:监听事件

接下来创建邮件监听器:

@Component
@RequiredArgsConstructor
public class EmailListener {

    private final EmailService emailService;

    @EventListener
    public void handle(UserRegisteredEvent event) {

        emailService.sendWelcomeEmail(
                event.email()
        );
    }
}

再创建积分监听器:

@Component
@RequiredArgsConstructor
public class PointsListener {

    private final PointsService pointsService;

    @EventListener
    public void handle(UserRegisteredEvent event) {

        pointsService.initPoints(
                event.userId()
        );
    }
}

再来一个优惠券监听器:

@Component
@RequiredArgsConstructor
public class CouponListener {

    private final CouponService couponService;

    @EventListener
    public void handle(UserRegisteredEvent event) {

        couponService.sendRegisterCoupon(
                event.userId()
        );
    }
}

现在业务结构就变成:

UserService
      │
      │ publishEvent
      ↓
UserRegisteredEvent
      │
      ├───────────────┐
      ↓               ↓
EmailListener    PointsListener
      │               │
      ↓               ↓
发送邮件          初始化积分

      │
      └───────────────→ CouponListener
                            ↓
                         发优惠券

而 UserService 根本不知道这些 Listener 的存在。


七、以后增加业务会发生什么?

例如产品经理突然提出一个需求:

用户注册之后,需要把注册行为上报给数据统计系统。

如果是以前的代码,你可能需要:

public void register(User user) {

    userRepository.save(user);

    emailService.sendWelcomeEmail(user);

    pointsService.initPoints(user.getId());

    couponService.sendCoupon(user.getId());

    statisticsService.report(user);
}

也就是说:

修改原有业务代码

但使用事件之后,只需要新增:

@Component
@RequiredArgsConstructor
public class StatisticsListener {

    private final StatisticsService statisticsService;

    @EventListener
    public void handle(UserRegisteredEvent event) {

        statisticsService.report(
                event.userId()
        );
    }
}

UserService:

public void register(User user)

完全不用动。

这就是 Spring Event 一个非常大的价值:

增加事件响应逻辑时,尽可能做到“新增代码”,而不是不断修改核心业务代码。


八、但是有一个非常重要的问题:Spring Event 默认是同步的

很多人第一次学 Spring Event 会误以为:

publishEvent()

类似:

RabbitMQ
Kafka

发布完以后马上返回。

实际上并不是。

Spring 默认的事件监听是同步执行的。默认的 SimpleApplicationEventMulticaster 会在发布者线程中调用监听器,因此 publishEvent() 可能一直阻塞到对应监听器处理完成。(Home )

例如:

eventPublisher.publishEvent(event);

可能发生:

HTTP 请求线程

    ↓

register()

    ↓

publishEvent()

    ↓

EmailListener
耗时 2 秒

    ↓

PointsListener
耗时 1 秒

    ↓

CouponListener
耗时 1 秒

    ↓

publishEvent() 返回

那么整个请求可能多等待:

2 + 1 + 1 = 4 秒

所以:

@EventListener

本身解决的是:

代码结构上的解耦。

它并不自动解决:

线程上的解耦。

这个区别非常重要。


九、使用 @Async 实现异步事件

如果邮件、日志、向量化、统计等任务比较耗时,通常可以异步执行。

例如:

@Component
@RequiredArgsConstructor
public class EmailListener {

    private final EmailService emailService;

    @Async
    @EventListener
    public void handle(UserRegisteredEvent event) {

        emailService.sendWelcomeEmail(
                event.email()
        );
    }
}

这时候大概变成:

请求线程
   ↓
保存用户
   ↓
publishEvent()
   ↓
提交异步任务
   ↓
register() 继续执行


线程池
   ↓
EmailListener
   ↓
发送邮件

Spring 的 @Async 会把任务提交给 TaskExecutor,调用线程可以在任务提交后继续执行。(Home )

不过使用 @Async 之前,一般还需要:

@EnableAsync

开启异步能力。


十、自定义事件线程池

实际项目中不太建议所有异步任务都混用一个默认线程池。

可以专门为事件处理定义线程池。

例如:

@Configuration
@EnableAsync
public class AsyncEventConfig {

    @Bean("eventTaskExecutor")
    public Executor eventTaskExecutor() {

        ThreadPoolTaskExecutor executor =
                new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(4);

        executor.setMaxPoolSize(8);

        executor.setQueueCapacity(100);

        executor.setKeepAliveSeconds(60);

        executor.setThreadNamePrefix(
                "event-handler-"
        );

        executor.setRejectedExecutionHandler(
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        executor.setWaitForTasksToCompleteOnShutdown(true);

        executor.setAwaitTerminationSeconds(30);

        executor.initialize();

        return executor;
    }
}

然后指定监听器使用这个线程池:

@Component
@Slf4j
public class UserRegisteredListener {

    @Async("eventTaskExecutor")
    @EventListener
    public void handle(
            UserRegisteredEvent event) {

        log.info(
                "处理注册事件:{}",
                event
        );
    }
}

Spring 官方支持直接在 @Async 中指定 Executor Bean:

@Async("eventTaskExecutor")

从而让不同类型的异步任务使用不同线程池。(Home )

这时候日志线程可能类似:

[event-handler-1]
[event-handler-2]
[event-handler-3]

而不是:

http-nio-8080-exec-1

说明任务已经进入异步线程池。


十一、同步事件和异步事件到底怎么选?

如果监听器逻辑非常轻量,并且:

监听器失败
=
主业务也应该失败

那么同步事件其实非常合适。

例如:

创建订单
↓
同步校验某些强业务规则

这种场景并不一定需要异步。

但是:

发送邮件
发送短信
记录非核心日志
生成缩略图
文件解析
AI 向量化
搜索索引更新
数据统计

这些操作通常不应该长时间阻塞主请求。

更加适合:

@EventListener
@Async

也就是说,判断的关键不是:

Event 要不要异步?

而是:

这个事件消费者是不是主业务成功的必要条件?


十二、异步 Event 还有一个非常重要的变化:异常

同步情况下:

Publisher
   ↓
Listener
   ↓
发生异常
   ↓
异常可能继续抛回 Publisher

如果 Publisher 正好在事务里面,那么监听器异常甚至可能影响原事务。

但是异步后:

请求线程
     ↓
发布事件
     ↓
异步线程处理
     ↓
Listener 抛异常

这个异常已经不能正常抛回原来的请求线程了。

Spring 官方文档明确说明,对于返回 void@Async 方法,异步线程中的异常无法传给原调用者;默认情况下异常通常会被记录,也可以配置 AsyncUncaughtExceptionHandler 做统一处理。(Home )

所以:

@Async

虽然方便,却不能意味着:

“任务一定成功”

真正重要的异步任务,还需要考虑:

异常处理
重试
告警
补偿
幂等

十三、数据库事务是 Spring Event 最容易踩坑的地方

现在考虑一个真实问题。

用户注册:

@Transactional
public void register(User user) {

    userRepository.save(user);

    eventPublisher.publishEvent(
            new UserRegisteredEvent(
                    user.getId(),
                    user.getEmail()
            )
    );
}

问题来了:

publishEvent()

执行的时候:

数据库事务可能还没有提交

流程其实可能是:

BEGIN

↓
INSERT user

↓
publishEvent()

↓
Listener 执行

↓
register() 结束

↓
COMMIT

如果 Listener 里面:

发送邮件

邮件已经发出去了。

结果最后:

COMMIT 失败

数据库里面根本没有这个用户。

于是出现:

用户注册失败
但是欢迎邮件已经发送成功

这显然不合理。


十四、@TransactionalEventListener

这种情况下可以使用:

@TransactionalEventListener

例如:

@Component
public class UserRegisteredListener {

    @TransactionalEventListener(
            phase = TransactionPhase.AFTER_COMMIT
    )
    public void handle(
            UserRegisteredEvent event) {

        System.out.println(
                "事务提交成功后执行"
        );
    }
}

这时候流程变成:

BEGIN
   ↓
保存用户
   ↓
发布事件
   ↓
register() 结束
   ↓
COMMIT
   ↓
事务提交成功
   ↓
执行 Listener

@TransactionalEventListener 默认阶段就是 AFTER_COMMIT,除此之外还支持 BEFORE_COMMITAFTER_ROLLBACKAFTER_COMPLETION。如果发布事件时根本不存在事务,默认不会执行该事务监听器,除非开启 fallbackExecution。(Home )

对于这种业务:

数据库成功
↓
才执行后续动作

它通常比普通:

@EventListener

更加合适。


十五、实际项目常见组合:事务提交成功 + 异步执行

例如:

用户注册
↓
数据库事务成功
↓
异步发送邮件

可以直接组合:

@Component
@RequiredArgsConstructor
public class UserRegisteredListener {

    private final EmailService emailService;

    @Async("eventTaskExecutor")
    @TransactionalEventListener(
            phase = TransactionPhase.AFTER_COMMIT
    )
    public void handle(
            UserRegisteredEvent event) {

        emailService.sendWelcomeEmail(
                event.email()
        );
    }
}

这样设计的语义非常清楚:

数据库事务提交成功
        ↓
再触发监听器
        ↓
交给异步线程池
        ↓
发送邮件

也就是:

主业务可靠性
+
代码解耦
+
线程解耦

这也是 Spring Event 在业务系统中非常实用的一种组合。


十六、放到“文件上传 + AI 向量化”场景就更容易理解了

例如上传 Markdown 文件。

如果全部写在 Controller 或 Service:

public void upload(MultipartFile file) {

    // 保存文件
    saveFile(file);

    // 保存数据库记录
    saveDatabase();

    // 解析 Markdown
    parseMarkdown();

    // 文档切片
    splitDocument();

    // 调用 Embedding 模型
    embedding();

    // 保存向量数据库
    saveVectorStore();
}

这就意味着一次 HTTP 请求可能要完成:

文件 IO
+
数据库 IO
+
Markdown 解析
+
Embedding 网络请求
+
向量数据库写入

上传接口很可能变得非常慢。

而且文件上传服务还直接依赖:

MarkdownParser
EmbeddingModel
VectorStore

耦合非常严重。

更好的思路是:

上传文件
     ↓
保存文件
     ↓
保存数据库
     ↓
发布 MarkdownUploadedEvent
     ↓
接口返回


后台事件线程
     ↓
解析 Markdown
     ↓
切分 Document
     ↓
Embedding
     ↓
写入 VectorStore

定义事件:

public record MarkdownUploadedEvent(
        Long fileId,
        String filePath
) {
}

上传:

@Transactional
public void upload(MultipartFile file) {

    String filePath = saveFile(file);

    Long fileId =
            saveFileRecord(filePath);

    eventPublisher.publishEvent(
            new MarkdownUploadedEvent(
                    fileId,
                    filePath
            )
    );
}

监听:

@Component
@RequiredArgsConstructor
public class MarkdownUploadedListener {

    private final MarkdownVectorService
            markdownVectorService;

    @Async("eventTaskExecutor")
    @TransactionalEventListener(
            phase = TransactionPhase.AFTER_COMMIT
    )
    public void vectorize(
            MarkdownUploadedEvent event) {

        markdownVectorService.vectorize(
                event.fileId(),
                event.filePath()
        );
    }
}

现在上传模块只需要负责:

文件上传成功

而向量化模块只负责:

收到文件上传成功事件
↓
执行向量化

这就是非常典型的业务解耦。


十七、Spring Event 什么时候特别适合用?

第一种情况是:

一个核心动作完成后
有多个附加动作

例如:

用户注册完成
→ 邮件
→ 积分
→ 优惠券
→ 日志
→ 统计

这类“一对多”的后续动作特别适合事件。

第二种情况是:

核心业务不应该知道后续具体实现

例如订单模块只应该知道:

订单支付成功

不应该知道:

库存怎么更新
积分怎么算
短信怎么发送
统计系统怎么上报

这时候可以发布:

OrderPaidEvent

第三种情况是:

耗时的非核心任务

例如:

图片处理
文件解析
AI 向量化
邮件
通知
数据统计
搜索索引

它们通常可以:

Event + Async

第四种情况是:

必须等数据库成功后才能做

例如:

订单真正创建成功
↓
发送订单通知

这种通常适合:

@TransactionalEventListener(
    phase = AFTER_COMMIT
)

十八、什么时候不应该使用 Spring Event?

Spring Event 并不是越多越好。

例如一个非常简单的强依赖关系:

订单计算价格
↓
优惠计算

订单价格根本离不开优惠计算结果。

那么直接:

priceService.calculate();

反而更清晰。

如果强行改成:

PriceCalculateEvent

代码反而变得很难追踪。

因为看到:

publishEvent()

你根本不知道:

谁处理了它?
处理顺序是什么?
产生了什么副作用?

所以 Event 更适合:

“某件事情已经发生”

而不是:

“请帮我调用某个方法并马上返回结果”

这是非常重要的判断标准。


十九、Spring Event 不是 MQ

这也是必须搞清楚的一点。

Spring Event 本质上主要是:

同一个 Spring 应用
同一个 JVM
同一个 ApplicationContext

里面的组件通信机制。Spring 官方也把它定位为同一应用上下文中 Spring Bean 之间的简单通信机制。(Home )

例如:

Spring Boot 应用

UserService
     ↓
Spring Event
     ↓
EmailListener

但是如果你的架构是:

用户服务
     ↓
???
     ↓
邮件服务

两个服务分别运行在:

服务器 A
服务器 B

那么 Spring Event 就不是合适的跨服务通信方案。

这时候通常需要:

RabbitMQ
Kafka
RocketMQ
Pulsar

这样的消息系统。

还有一个特别重要的区别:

Spring Event 本身不是持久化消息队列。

如果你:

publishEvent()
↓
异步任务还没完成
↓
应用直接宕机

普通 Spring Event 并没有天然提供类似 MQ 的:

消息持久化
ACK
重试队列
消费确认
死信队列
跨进程投递

所以像:

支付成功
↓
必须保证财务系统最终收到

这种高可靠事件,不能只因为:

@Async
@EventListener

就认为已经可靠了。

这时候往往应该继续升级到:

事务消息
Outbox Pattern
RabbitMQ
Kafka
RocketMQ

等方案。


二十、普通 Event、Async Event、事务 Event、MQ 怎么选择?

可以建立这样一个判断思路:

只是同一个项目内部解耦?
        ↓
      Event


监听任务比较耗时?
        ↓
 Event + @Async


必须数据库提交成功之后执行?
        ↓
@TransactionalEventListener


数据库成功之后还要异步?
        ↓
@TransactionalEventListener
        +
      @Async


跨服务通信?
        ↓
       MQ


消息绝对不能随应用宕机而丢失?
        ↓
MQ / Outbox / 可靠消息方案

这个判断逻辑,比单纯背:

@EventListener

有用得多。


二十一、还有几个很容易踩的坑

首先,不要往 Event 里面塞整个复杂业务对象。

例如:

new UserRegisteredEvent(user)

有时候方便,但事件异步执行的时候,这个对象后续可能发生变化。

更稳妥的方式通常是携带:

userId
email
orderId
fileId
filePath

这种事件真正需要的数据。

其次,不要因为用了:

@Async

就认为任务一定完成。

异步只是:

换线程执行

不是:

可靠消息

而且异步 void 监听器的异常不会重新抛回发布者,需要单独做好异常记录、告警或补偿。(Home )

再次,线程池一定要考虑容量。

例如使用:

new ThreadPoolExecutor.CallerRunsPolicy()

意味着线程池和队列全部满了以后,任务可能改由提交任务的线程执行。

于是原本:

HTTP 请求线程
↓
提交异步任务
↓
快速返回

在高负载时可能退化成:

HTTP 请求线程
↓
自己执行事件任务
↓
接口变慢

所以线程池参数不能简单照抄,需要结合实际任务耗时和流量设计。


二十二、事务监听还有一个高级坑

如果使用:

@TransactionalEventListener(
    phase = TransactionPhase.AFTER_COMMIT
)

那么进入监听器时,原事务已经提交。

Spring 官方特别提醒,虽然这时某些事务资源可能仍然处于可访问状态,但原事务已经结束,不能简单认为后续数据库修改还会随着原事务再次提交。需要进行新的事务性数据库操作时,应该明确开启新的事务。(Home )

例如可以把数据库修改交给另一个 Service:

@Service
public class PointsService {

    @Transactional(
        propagation = Propagation.REQUIRES_NEW
    )
    public void initPoints(Long userId) {

        // 新事务
    }
}

然后:

@TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT
)
public void handle(
        UserRegisteredEvent event) {

    pointsService.initPoints(
            event.userId()
    );
}

这样事务边界会更加清晰。


二十三、回头再看“解耦”

没有事件之前:

UploadService
    │
    ├── MarkdownParser
    ├── EmbeddingModel
    ├── VectorStore
    ├── NotificationService
    └── ...

使用事件之后:

UploadService
      ↓
MarkdownUploadedEvent
      ↓
Spring Event
      │
      ├── VectorListener
      ├── LogListener
      └── NotificationListener

于是上传模块只表达:

Markdown 文件已经上传成功。

至于后面:

解析
切片
Embedding
向量存储
通知
统计

全部由其他模块自己决定。

这才是事件真正的意义。

它不是单纯为了少写几个:

service.xxx();

而是把系统从:

“我要调用谁”

转变成:

“发生了什么”

总结

Spring Event 入门真正需要掌握的核心流程其实非常简单:

定义 Event
    ↓
ApplicationEventPublisher
    ↓
publishEvent()
    ↓
@EventListener
    ↓
处理事件

如果任务比较耗时:

@EventListener
+
@Async

如果要求:

数据库成功以后
才允许处理事件

使用:

@TransactionalEventListener(
    phase = AFTER_COMMIT
)

如果既要求事务成功,又不想阻塞当前线程:

@TransactionalEventListener
+
@Async

而如果已经涉及:

跨服务
消息不能丢
可靠重试
消费确认
高吞吐消息流

那么就应该开始考虑:

RabbitMQ
Kafka
RocketMQ
Outbox Pattern

而不是继续把 Spring Event 当消息队列使用。

所以理解 Spring Event 最重要的一句话是:

Spring Event 的核心不是“异步”,而是通过“事件”切断业务模块之间的直接调用关系。

发布者只需要告诉系统:

“某件事情已经发生了。”

至于谁关心、谁处理、未来增加多少个处理逻辑,都交给监听者自己完成。

这才是真正的业务解耦。

Spring Event 入门:用事件发布订阅实现业务解耦
http://www.clxhxhhr.top/posts/634/
作者
clxstart
发布于
2026-09-16
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。