👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、IoT物联网等功能:

  • 多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • 微服务:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本 


问题:越写越胖的 Service 方法

工作年头长了,见过太多这样的 Service 方法:

public void createOrder(Order order) {
    // 1. 保存订单
    orderDao.save(order);
    // 2. 发送短信通知
    smsService.sendOrderConfirm(order.getUserPhone(), order.getId());
    // 3. 更新用户积分
    pointService.addPoints(order.getUserId(), order.getTotalAmount());
    // 4. 记录操作日志
    auditService.log("CREATE_ORDER", order.getId());
    // 5. 推送站内消息
    notifyService.push(order.getUserId(), "您的订单已提交");
    // 6. 触发风控检查
    riskService.check(order);
}

技术上没 Bug,跑得也好好的。但有一天产品说"下单后还要同步购物车",你得改;运营说"VIP 下单要额外赠券",你还得改。核心下单逻辑就这样被各种非核心需求越填越厚,所有依赖硬编码在一起。哪天 riskService 挂了,整个下单都跟着挂 ——这明显不合理。

很多人的第一反应是上 MQ。但如果是单体应用,或者只是同 JVM 内的逻辑解耦 ,部署一套 MQ 中间件有点杀鸡用牛刀。

Spring 早就提供了一套轻量级事件驱动机制 ——ApplicationEvent + @EventListener,本质是发布-订阅 模式,零额外依赖,Spring Boot 开箱即用。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

四个核心角色

Spring事件机制核心组件全景:事件、发布者、广播器、监听者四大角色的关系图

1. ApplicationEvent(事件)

数据载体。继承自 JDK EventObject,包含 source 和 timestamp。Spring 4.2 后支持发布任意对象(不继承也行),内部自动封装为 PayloadApplicationEvent

2. ApplicationEventPublisher(发布者)

只有一个方法 publishEvent()ApplicationContext 实现了这个接口,注入 ApplicationEventPublisher 更优雅——依赖接口而非实现 。

3. ApplicationEventMulticaster(广播器)

整个机制的核心调度引擎 。维护所有已注册的监听器列表,负责将事件分发给匹配的监听者。默认实现 SimpleApplicationEventMulticaster

4. ApplicationListener(监听者)

接收并处理事件,通过实现接口或注解定义。

角色

对应组件

责任

事件

ApplicationEvent

信息载体

发布者

ApplicationEventPublisher

触发事件

广播器

ApplicationEventMulticaster

调度分发

监听者

ApplicationListener

 / @EventListener

处理事件

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

两种使用方式:接口实现 vs 注解

方式一:实现 ApplicationListener 接口(传统方式)

Step 1:定义事件对象

public class OrderCreatedEvent extends ApplicationEvent {
    privatefinal String orderId;
    privatefinal Long userId;
    privatefinal BigDecimal totalAmount;

    public OrderCreatedEvent(Object source, String orderId, Long userId, BigDecimal totalAmount) {
        super(source);
        this.orderId = orderId;
        this.userId = userId;
        this.totalAmount = totalAmount;
    }

    public String getOrderId() { return orderId; }
    public Long getUserId() { return userId; }
    public BigDecimal getTotalAmount() { return totalAmount; }
}

Step 2:发布事件

@Service
publicclass OrderService {
    privatefinal ApplicationEventPublisher eventPublisher;

    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public Order createOrder(CreateOrderRequest req) {
        Order order = buildOrder(req);
        orderDao.save(order);
        eventPublisher.publishEvent(
            new OrderCreatedEvent(this, order.getId(), order.getUserId(), order.getTotalAmount())
        );
        return order;
    }
}

Step 3:实现监听器

@Component
publicclass SmsNotifyListener implements ApplicationListener<OrderCreatedEvent> {
    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        smsService.send(event.getUserId(), "订单 " + event.getOrderId() + " 已提交");
    }
}

@Component
publicclass PointsListener implements ApplicationListener<OrderCreatedEvent> {
    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        pointService.addPoints(event.getUserId(), event.getTotalAmount());
    }
}

缺点:一个监听器只能监听一种事件类型 ,类里只能有一个处理方法。

方式二:@EventListener 注解(推荐)

Spring 4.2 引入,灵活度大幅提升,主流写法:

@Component
publicclass OrderEventHandler {

    @EventListener
    public void handleSms(OrderCreatedEvent event) {
        smsService.send(event.getUserId(), "订单已提交");
    }

    // 同时监听多种事件
    @EventListener(value = {OrderCreatedEvent.class, OrderPaidEvent.class})
    public void handleOrderStateChange(ApplicationEvent event) {
        auditService.log(event);
    }

    // 条件监听:只处理金额 > 1000 元的
    @EventListener(condition = "#event.totalAmount > 1000")
    public void handleHighValueOrder(OrderCreatedEvent event) {
        vipService.notifyHighValue(event.getOrderId());
    }

    // 返回新事件:自动广播下一个
    @EventListener
    public InventoryUpdateEvent handleOrder(OrderCreatedEvent event) {
        returnnew InventoryUpdateEvent(this, event.getOrderId());
    }
}

几个少被提及的功能:**condition** 支持 SpEL 过滤;返回值如果是事件对象会自动发布,适合事件链;@Order 控制执行顺序。

@EventListener
@Order(1)  // 先执行
@Async
public void riskCheck(OrderCreatedEvent event) { ... }

@EventListener
@Order(2)  // 后执行
@Async
public void sendSms(OrderCreatedEvent event) { ... }

Spring 内置事件

Spring 在容器生命周期关键节点也会发布事件:

Spring内置事件类型列表
Spring内置事件类型列表

事件

触发时机

常用场景

ContextRefreshedEvent

容器初始化或刷新完成

预热缓存、加载配置

ContextStartedEvent

调用 context.start()

启动定时任务

ContextStoppedEvent

调用 context.stop()

清理资源

ContextClosedEvent

容器关闭

优雅关机

RequestHandledEvent

HTTP 请求处理完毕

请求监控

最常用的是 ContextRefreshedEvent预热缓存 的经典用法:

@Component
public class CachePreloadListener {
    @EventListener(ContextRefreshedEvent.class)
    public void preloadCache(ContextRefreshedEvent event) {
        // 父子容器场景会触发两次,需要防守
        if (event.getApplicationContext().getParent() != null) {
            return;
        }
        CompletableFuture.runAsync(() -> {
            provinceService.loadAll();
            hotProductService.preload();
            log.info("缓存预热完成");
        });
    }
}

Spring Boot 里有更好用的 ApplicationReadyEvent,只触发一次:

@EventListener(ApplicationReadyEvent.class)
public void onReady() {
    cacheService.initAll();
}

同步还是异步?

Spring 事件默认同步 ——publishEvent() 会阻塞,等所有监听器执行完才继续。好处是简单、对事务友好;坏处是监听器卡顿会拖慢发布者 。

开启异步

三步搞定:

第一步:开启异步支持

@SpringBootApplication
@EnableAsync
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

第二步:配置专用线程池 (强烈建议,否则会用 ForkJoinPool.commonPool

@Configuration
public class AsyncConfig {
    @Bean("eventExecutor")
    public Executor eventTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("event-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

第三步:给监听器加 @Async

@Async("eventExecutor")
@EventListener
public void handleSms(OrderCreatedEvent event) {
    smsService.send(event.getUserId(), "订单已提交");
}
两个必知的异步局限

1. 异常不传播 :异步监听器的异常在线程池线程中自行消化,发布者完全感知不到。必须在监听器内 try-catch。

2. 返回值发布失效 :加了 @Async 后,返回事件对象不会自动发布,需手动 publishEvent

@Async("eventExecutor")
@EventListener
public void handleSms(OrderCreatedEvent event) {
    try {
        smsService.send(event.getUserId(), "订单已提交");
    } catch (Exception e) {
        log.error("短信发送失败,订单 ID: {}", event.getOrderId(), e);
        alarmService.notify("SMS_FAILED", e.getMessage());
    }
}

@TransactionalEventListener:事务提交后再执行

场景:用户修改订单后删缓存。如果用普通 @EventListener,缓存删了但事务可能回滚——缓存击穿 。

@TransactionalEventListener 解决这个问题:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void cleanCache(OrderUpdatedEvent event) {
    redisTemplate.delete("order:" + event.getOrderId());
}

phase

触发时机

AFTER_COMMIT

(默认)

事务成功提交后

AFTER_ROLLBACK

事务回滚后

AFTER_COMPLETION

事务完成后(无论成败)

BEFORE_COMMIT

事务提交前

两个隐形陷阱 :

1. 没有事务时事件会被丢弃

// 没有 @Transactional 的方法发布事件
public void someMethod() {
    eventPublisher.publishEvent(new OrderUpdatedEvent(...));
    // @TransactionalEventListener 会直接丢弃!
}

加 fallbackExecution = true 解决:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void cleanCache(OrderUpdatedEvent event) {
    redisTemplate.delete("order:" + event.getOrderId());
}

2. AFTER_COMMIT 阶段里起不了新事务

事务已提交,加了 @Transactional 默认也不会新开事务,必须指定 Propagation.REQUIRES_NEW

源码解析:事件是怎么走到监听器的

注册阶段

容器 refresh() 过程中完成两件事:

初始化广播器 :

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster = beanFactory.getBean(...);
    } else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
    }
}

注册监听器 :先注册编程式添加的监听器,再从 BeanFactory 找所有 ApplicationListener 接口的 Bean。@EventListener 注解由 EventListenerMethodProcessor 处理——在所有单例 Bean 初始化完成后扫描带注解的方法,封装为 ApplicationListenerMethodAdapter 注册到广播器。

分发阶段
// SimpleApplicationEventMulticaster#multicastEvent()
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        } else {
            invokeListener(listener, event);
        }
    }
}

getApplicationListeners() 内部有缓存,首次调用遍历所有监听器做类型匹配,之后走缓存。

完整调用链:

publishEvent()
  └─ multicastEvent()
      └─ getApplicationListeners()(带缓存的匹配)
      └─ invokeListener()
          └─ doInvokeListener()
              └─ listener.onApplicationEvent()  ← 你的业务代码

踩坑实录

坑 1:事件对象设计成可变的
@EventListener
public void handleA(OrderCreatedEvent event) {
    event.setStatus("PROCESSING");  // 修改了事件对象
}

@EventListener
public void handleB(OrderCreatedEvent event) {
    // handleA 已经改了 status,这里看到脏数据
    doSomething(event.getStatus());
}

多个监听器共享同一个事件对象。事件对象必须不可变 :属性加 final,不提供 setter。

坑 2:开了 @Async 却没开 @EnableAsync
@EventListener
@Async
public void asyncHandle(OrderCreatedEvent event) {
    // 看起来异步,实际同步执行,而且无任何报错
}

@Async 不生效的表现是性能没提升却找不到原因。**@EnableAsync 必须加在主类或配置类上。**

坑 3:事件循环调用
@EventListener
public void handleA(EventA event) {
    eventPublisher.publishEvent(new EventB());
}
@EventListener
public void handleB(EventB event) {
    eventPublisher.publishEvent(new EventA());  // 死循环!StackOverflowError
}

设计事件时要明确依赖关系,避免形成环。

坑 4:@TransactionalEventListener 无事务时默默丢事件

上面已经提到。没事务 + 没设 fallbackExecution = true = 事件被默默丢弃,调试时极难发现。

Spring Event vs MQ:怎么选?

Spring Event 与 MQ 对比选型决策树
Spring Event 与 MQ 对比选型决策树

对比维度

Spring Event

MQ(Kafka/RocketMQ)

适用场景

同 JVM 内模块解耦

跨服务通信

可靠性

进程崩溃就丢事件

持久化 + 重试

延迟

内存级,≈ 0

受网络影响

开发成本

零依赖,注解即用

需部署中间件

一致性

本地事务保障

需分布式事务

吞吐量

异步+批量 ~9w/s

受网络并发限制

简单说:单体应用 / 同 JVM 逻辑解耦 / 不要求消息持久化 → Spring Event ;微服务跨服务通信 / 高可靠消息传输 → MQ 。

生产级最佳实践

1. 事件对象保持轻量 :只传 ID,不传大对象。

public class OrderCreatedEvent extends ApplicationEvent {
    private final String orderId;
    private final String version = "1.0";
}

2. 一个监听器只做一件事 :PaymentListener 只处理支付,CouponListener 只发券。

3. 异步监听器必须自己捕异常 。

4. 加监控 :关键链路的监听器加耗时统计和告警。

5. 事件命名要语义化 :用过去式动词——OrderCreatedEventPaymentSuccessEvent。不要用 DoSomethingEvent

回到开头那个问题

那个越写越胖的 createOrder,用事件机制改造:

@Transactional
public Order createOrder(CreateOrderRequest req) {
    Order order = buildOrder(req);
    orderDao.save(order);
    eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
    return order;
}

// 新增功能?加个监听器就好,不动核心代码
@Component
publicclass NewFeatureListener {
    @EventListener
    public void handleNewFeature(OrderCreatedEvent event) {
        // 新功能在这里
    }
}

一个经得起考验的系统,不是靠把所有逻辑塞进一个方法里,而是让不同模块各司其职。Spring 事件机制,正是实现这种协作的工具。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐