什么是Spring事件?
想象一下你去吃火锅,点完菜后,服务员按下呼叫器:"A区3号桌已下单!",这个消息一广播,后厨开始切菜,配菜员准备料碗,传菜的小哥也是立马进入待命状态。
每个岗位听到出餐事件后,各自执行自己该干的活,互不干扰,但又配合默契。
从技术上来说,Spring事件其实就是观察者模式的实现,主要包含三部分::
- 事件源(Event Source):谁触发的事件
- 事件(Event):发生了什么事,带了什么数据
- 监听器(Listener):谁来处理这个事件
为什么我们需要Spring事件?
举个栗子:用户注册功能
假设用户注册成功后需要做这些事:
- 发欢迎邮件
- 初始化用户积分
- 记录操作日志
- 推送新用户通知
传统写法可能是这样的:
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事件?
推荐使用的场景:
- 用户行为追踪:注册、登录、支付等重要操作
- 数据同步:缓存更新、数据备份、搜索引擎索引更新
- 消息通知:邮件、短信、推送通知
- 统计计算:浏览量、点击量统计
- 系统监控:异常报警、性能监控
- 业务流程解耦:订单状态变更、库存更新等
不推荐使用的场景:
- 需要严格保证顺序的业务流程
- 需要立即获取处理结果的场景
- 简单的CRUD操作
- 高性能要求的核心业务
- 需要强一致性的业务
使用小贴士
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 种耗时统计方式,你用过几种?》