Spring事件的3种高级玩法,90%的人根本不会用

什么是Spring事件?

想象一下你去吃火锅,点完菜后,服务员按下呼叫器:"A区3号桌已下单!",这个消息一广播,后厨开始切菜,配菜员准备料碗,传菜的小哥也是立马进入待命状态。

每个岗位听到出餐事件后,各自执行自己该干的活,互不干扰,但又配合默契。

从技术上来说,Spring事件其实就是观察者模式的实现,主要包含三部分::

  • 事件源(Event Source):谁触发的事件
  • 事件(Event):发生了什么事,带了什么数据
  • 监听器(Listener):谁来处理这个事件

为什么我们需要Spring事件?

举个栗子:用户注册功能

假设用户注册成功后需要做这些事:

  1. 发欢迎邮件
  2. 初始化用户积分
  3. 记录操作日志
  4. 推送新用户通知

传统写法可能是这样的:

java 复制代码
@Service
public class UserService {
    // 注入一堆服务
    @Autowired 
    private EmailService emailService;
    
    @Autowired 
    private PointService pointService;
    // ... 还有其他依赖
    
    public void register(User user) {
        // 先保存用户
        userDao.save(user);
        
        // 然后开始各种后续操作
        emailService.sendWelcomeEmail(user);  // 发邮件
        pointService.initUserPoints(user);    // 初始化积分
        logService.recordUserLog(user);       // 记录日志
        noticeService.pushNewUserNotice(user); // 推送通知
        
        // 要是再加新功能,这里还得继续加...
    }
}

这样写的问题:

  • 代码绑得太紧:注册方法要知道所有后续操作
  • 越改越乱:每次加新功能都要改注册方法
  • 一个类干太多事:UserService变得特别臃肿
  • 性能瓶颈:所有操作都得排队执行
  • 测试麻烦:测试注册功能得mock一堆服务

用Spring事件来优化

1. 先定义个事件

java 复制代码
public class UserRegisterEvent extends ApplicationEvent {
    private User user;
    
    public UserRegisterEvent(Object source, User user) {
        super(source);
        this.user = user;
    }
    
    public User getUser() {
        return user;
    }
}

2. 发布事件

java 复制代码
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void register(User user) {
        // 注册用户
        userDao.save(user);
        
        // 发个通知:用户注册成功啦!
        eventPublisher.publishEvent(new UserRegisterEvent(this, user));
    }
}

3. 监听事件

java 复制代码
@Component
public class EmailListener {
    @Async  // 异步处理,不阻塞主流程
    @EventListener
    public void handleUserRegister(UserRegisterEvent event) {
        // 专门处理发邮件
        emailService.sendWelcomeEmail(event.getUser());
    }
}

@Component
public class PointListener {
    @Async
    @EventListener
    public void initUserPoints(UserRegisterEvent event) {
        // 专门处理积分初始化
        pointService.initUserPoints(event.getUser());
    }
}

Spring事件的优点

1. 解耦神器

各个业务模块不再直接互相调用,通过事件来通信,每个模块只管自己的事。

2. 扩展超方便

加新功能?只需要新写个监听器就行,完全不用动原来的代码。

3. 支持异步处理

加上@Async注解,耗时操作可以异步执行,不影响主流程响应速度。

4. 灵活的事务控制

可以配置事件在事务提交前或提交后触发,适应不同业务需求。

5. 执行顺序可控

@Order注解可以指定哪个监听器先执行。

6. 条件监听

可以设置条件,只有满足条件的事件才处理。

Spring事件的缺点

1. 调试有点头疼

事件链路比较长,出了问题要追好几层,需要有完善的日志。

2. 代码流程不够直观

事件是"隐式"的调用,新同事可能要花时间理清事件关系。

3. 用多了反而复杂

如果每个小操作都用事件,项目会变得很难理解。

4. 错误处理要小心

异步事件下的异常处理需要特别注意,可能需要重试机制。

5. 内存泄漏风险

如果监听器持有大量资源且没有正确释放,可能会有问题。

6. 循环依赖风险

事件发布者和监听器之间可能形成循环依赖。

什么时候该用Spring事件?

推荐使用的场景:

  1. 用户行为追踪:注册、登录、支付等重要操作
  2. 数据同步:缓存更新、数据备份、搜索引擎索引更新
  3. 消息通知:邮件、短信、推送通知
  4. 统计计算:浏览量、点击量统计
  5. 系统监控:异常报警、性能监控
  6. 业务流程解耦:订单状态变更、库存更新等

不推荐使用的场景:

  1. 需要严格保证顺序的业务流程
  2. 需要立即获取处理结果的场景
  3. 简单的CRUD操作
  4. 高性能要求的核心业务
  5. 需要强一致性的业务

使用小贴士

1. 事件命名要清楚

比如UserRegisteredEvent就比UserEvent更明确,一看就知道是用户注册事件。

2. 日志要打全

事件发布和监听的地方都要记录日志,方便排查问题。

java 复制代码
@EventListener
public void handleUserRegister(UserRegisterEvent event) {
    log.info("开始处理用户注册事件,用户ID:{}", event.getUser().getId());
    try {
        emailService.sendWelcomeEmail(event.getUser());
        log.info("用户注册事件处理完成");
    } catch (Exception e) {
        log.error("处理用户注册事件失败", e);
        throw e;
    }
}

3. 合理使用异步

非核心业务用异步处理,提升性能:

java 复制代码
@Async
@EventListener
@Order(1) // 指定执行顺序
public void asyncHandleEvent(UserRegisterEvent event) {
    // 异步处理逻辑
}

4. 注意事务边界

使用@TransactionalEventListener控制事件与事务的关系:

java 复制代码
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommitHandleEvent(UserRegisterEvent event) {
    // 事务提交后执行,确保数据一致性
}

5. 事件数据要不可变

事件对象应该是只读的,避免在监听器中修改:

java 复制代码
public class UserRegisterEvent extends ApplicationEvent {
    private final User user; // 用final确保不可变
    
    // ... 构造方法和getter
}

高级玩法

1. 条件监听

java 复制代码
@EventListener(condition = "#event.user.vip == true")
public void handleVipUserRegister(UserRegisterEvent event) {
    // 只处理VIP用户注册
}

2. 事件链(一个事件触发另一个事件)

实现事件的级联触发:

java 复制代码
@Component
public class OrderEventChain {
    
    @EventListener
    @Order(1)
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 订单创建后,触发库存检查
        if (inventoryService.checkStock(event.getOrder())) {
            // 库存充足,触发下一步
            eventPublisher.publishEvent(new StockAvailableEvent(this, event.getOrder()));
        } else {
            eventPublisher.publishEvent(new StockShortageEvent(this, event.getOrder()));
        }
    }
    
    @EventListener
    @Order(2)
    public void handleStockAvailable(StockAvailableEvent event) {
        // 库存充足,开始处理支付
        paymentService.processPayment(event.getOrder());
        eventPublisher.publishEvent(new PaymentProcessedEvent(this, event.getOrder()));
    }
    
    @EventListener
    @Order(3)
    public void handlePaymentProcessed(PaymentProcessedEvent event) {
        // 支付完成,开始发货
        shippingService.shipOrder(event.getOrder());
        eventPublisher.publishEvent(new OrderCompletedEvent(this, event.getOrder()));
    }
}

3. 事件拦截器(AOP方式)

用AOP来给事件处理加"佐料":

java 复制代码
@Aspect
@Component
public class EventListenerAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(EventListenerAspect.class);
    
    @Around("@annotation(org.springframework.context.event.EventListener)")
    public Object aroundEventListener(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object event = joinPoint.getArgs()[0];
        
        logger.info("开始处理事件: {}", event.getClass().getSimpleName());
        
        try {
            Object result = joinPoint.proceed();
            long cost = System.currentTimeMillis() - startTime;
            logger.info("事件处理完成: {}, 耗时: {}ms", event.getClass().getSimpleName(), cost);
            return result;
        } catch (Exception e) {
            logger.error("事件处理失败: {}", event.getClass().getSimpleName(), e);
            throw e;
        }
    }
    
    // 监控事件处理性能
    @AfterReturning("@annotation(org.springframework.context.event.EventListener)")
    public void monitorEventListener(JoinPoint joinPoint) {
        Metrics.counter("event.processed")
              .tag("event", joinPoint.getArgs()[0].getClass().getSimpleName())
              .increment();
    }
}

4. 事件数据 enrichment(丰富事件数据)

在处理事件前,先补充一些数据:

java 复制代码
@Component
public class EventEnrichmentService {
    
    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级,最先执行
    public void enrichUserEvent(UserRegisterEvent event) {
        // 在事件处理前补充数据
        User user = event.getUser();
        
        // 补充用户IP地理位置
        String location = ipLocationService.getLocation(user.getRegisterIp());
        user.setRegisterLocation(location);
        
        // 补充用户设备信息
        String deviceInfo = userAgentParser.parse(user.getUserAgent());
        user.setDeviceInfo(deviceInfo);
    }
}

// 使用 enriched 数据
@Component
public class AnalyticsListener {
    
    @EventListener
    @Order(Ordered.LOWEST_PRECEDENCE) // 低优先级,最后执行
    public void analyzeUserRegister(UserRegisterEvent event) {
        // 这里可以使用 enrichment 后的数据
        String location = event.getUser().getRegisterLocation();
        String deviceInfo = event.getUser().getDeviceInfo();
        
        analyticsService.trackRegistration(event.getUser(), location, deviceInfo);
    }
}

总结

Spring事件是个很好的解耦工具,用对了能让代码更清晰、更好维护。但它也不是万能的,要根据实际情况合理使用。

使用心得:

  • 主要业务逻辑别过度设计
  • 次要的、可选的业务适合用事件
  • 每个事件职责要单一
  • 异常处理和日志很重要
  • 同步/异步要根据业务需求选择

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot 中的 7 种耗时统计方式,你用过几种?》

《千万级大表如何新增字段?别再直接 ALTER 了》

《Vue3 如何优雅地实现一个全局的 loading 组件》

《Vue3+CSS实现一个非常丝滑的 input 标签上浮动画,设计师看了都点赞》

相关推荐
liyi_hz20083 小时前
O2OA (翱途)开发平台新版本发布预告:架构升级、性能跃迁、功能全面进化
android·java·javascript·开源软件
熊猫钓鱼>_>3 小时前
Java String 性能优化与内存管理:现代开发实战指南
java·开发语言·性能优化
练习时长一年3 小时前
Spring容器的refresh()方法
java·开发语言
唐叔在学习3 小时前
Pyinstaller - Python桌面应用打包的首选工具
后端·python·程序员
我叫黑大帅3 小时前
用户头像文件存储功能是如何实现的?
后端·google
程序员小假3 小时前
MySQL 与 Redis 如何保证双写一致性?
java·后端
Arlene3 小时前
JVM Java虚拟机
java·开发语言·jvm
千码君20163 小时前
Go语言:关于导包的两个重要说明
开发语言·后端·golang·package·导包
oak隔壁找我3 小时前
Java 高级特性
java·后端