关注CodingTechWork
引言
在分布式系统设计中,模块间的解耦是一个永恒的话题。Spring框架提供的事件机制为我们提供了一种优雅的解决方案。本文将深入探讨Spring事件机制的实现原理,并通过一个商品删除的实际场景,展示如何利用事件机制实现模块间的松耦合通信。
Spring事件机制概述
什么是事件驱动架构
事件驱动架构是一种软件设计范式,其中系统的各个组件通过事件进行通信。当某个组件状态发生变化时,它会发布事件,其他组件监听这些事件并做出相应处理。
Spring事件机制的核心组件
- 事件(Event): 封装了需要传递的信息
- 监听器(Listener): 接收并处理特定类型的事件
- 发布器(Publisher): 负责发布事件
- 事件广播器(ApplicationEventMulticaster): 管理监听器并广播事件
@EventListener注解原理深度解析
注解的定义
java
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {
// 指定要监听的事件类型,默认为方法参数类型
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
// SpEL表达式,用于条件过滤
String condition() default "";
// 监听器的唯一标识
String id() default "";
}
工作原理
Spring在处理@EventListener注解时,主要通过以下几个步骤:
java
// 简化的原理示意代码
public class EventListenerMethodProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 1. 扫描所有带有@EventListener注解的方法
Method[] methods = bean.getClass().getDeclaredMethods();
for (Method method : methods) {
EventListener ann = method.getAnnotation(EventListener.class);
if (ann != null) {
// 2. 解析方法参数,确定监听的事件类型
Class<?> eventType = method.getParameterTypes()[0];
// 3. 创建监听器适配器
ApplicationListener<?> listener =
new ApplicationListenerMethodAdapter(beanName, bean.getClass(), method);
// 4. 注册到应用上下文
applicationContext.addApplicationListener(listener);
}
}
return bean;
}
}
事件发布原理
java
// Spring事件发布核心流程
public abstract class AbstractApplicationContext {
public void publishEvent(ApplicationEvent event) {
// 1. 获取事件广播器
ApplicationEventMulticaster multicaster = getApplicationEventMulticaster();
// 2. 广播事件到所有匹配的监听器
multicaster.multicastEvent(event);
}
}
public class SimpleApplicationEventMulticaster {
public void multicastEvent(ApplicationEvent event) {
// 1. 获取所有匹配的监听器
Collection<ApplicationListener<?>> listeners = getApplicationListeners(event);
// 2. 遍历执行监听器
for (ApplicationListener<?> listener : listeners) {
// 支持同步或异步执行
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
} else {
invokeListener(listener, event);
}
}
}
}
实战:商品删除事件处理Demo
项目结构
src/main/java/com/example/demo/
├── DemoApplication.java
├── event/
│ ├── ProductDeleteEvent.java
│ └── ProductEventPublisher.java
├── listener/
│ ├── InventoryListener.java
│ ├── OrderListener.java
│ └── StatisticsListener.java
├── model/
│ └── Product.java
└── service/
└── ProductService.java
完整代码实现
实体类
java
package com.example.demo.model;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class Product {
private Long id;
private String name;
private String category;
private BigDecimal price;
private Integer stock;
private LocalDateTime deleteTime;
public Product(Long id, String name, String category, BigDecimal price, Integer stock) {
this.id = id;
this.name = name;
this.category = category;
this.price = price;
this.stock = stock;
}
}
自定义事件
java
package com.example.demo.event;
import com.example.demo.model.Product;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
@Getter
public class ProductDeleteEvent extends ApplicationEvent {
private final Product product;
private final String deleteReason;
private final Long operatorId;
public ProductDeleteEvent(Object source, Product product, String deleteReason, Long operatorId) {
super(source);
this.product = product;
this.deleteReason = deleteReason;
this.operatorId = operatorId;
}
// 添加额外的方法,方便在条件表达式中使用
public boolean isExpensiveProduct() {
return product.getPrice().compareTo(new BigDecimal("1000")) > 0;
}
}
事件发布器
java
package com.example.demo.event;
import com.example.demo.model.Product;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductEventPublisher {
private final ApplicationEventPublisher eventPublisher;
public void publishProductDeleteEvent(Product product, String deleteReason, Long operatorId) {
log.info("发布商品删除事件: 商品ID={}, 商品名称={}, 删除原因={}",
product.getId(), product.getName(), deleteReason);
ProductDeleteEvent event = new ProductDeleteEvent(this, product, deleteReason, operatorId);
eventPublisher.publishEvent(event);
}
}
监听器实现
库存模块监听器:
java
package com.example.demo.listener;
import com.example.demo.event.ProductDeleteEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class InventoryListener {
@EventListener
public void handleProductDelete(ProductDeleteEvent event) {
log.info("【库存模块】接收到商品删除事件");
log.info("开始清理库存数据: 商品ID={}, 商品名称={}",
event.getProduct().getId(), event.getProduct().getName());
// 模拟库存清理逻辑
try {
// 1. 清理库存记录
Thread.sleep(500); // 模拟耗时操作
log.info("库存记录清理完成: 商品ID={}", event.getProduct().getId());
// 2. 释放仓库空间
log.info("仓库空间释放完成");
} catch (Exception e) {
log.error("库存清理失败", e);
}
}
}
订单模块监听器:
java
package com.example.demo.listener;
import com.example.demo.event.ProductDeleteEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class OrderListener {
@Async // 异步处理,不影响主流程
@EventListener
public void handleProductDelete(ProductDeleteEvent event) {
log.info("【订单模块】异步处理商品删除事件");
// 模拟订单相关处理
log.info("开始处理相关订单: 商品ID={}", event.getProduct().getId());
try {
// 1. 查询未完成的订单
Thread.sleep(1000);
log.info("发现3个未完成订单,已通知客服处理");
// 2. 更新订单状态
log.info("相关订单状态已更新为'商品已下架'");
} catch (Exception e) {
log.error("订单处理失败", e);
}
}
@EventListener(condition = "#event.deleteReason == '违规商品'")
public void handleIllegalProductDelete(ProductDeleteEvent event) {
log.info("【订单模块】处理违规商品删除 - 特殊流程");
log.info("商品名称: {}, 需要通知所有买家", event.getProduct().getName());
}
}
统计模块监听器:
java
package com.example.demo.listener;
import com.example.demo.event.ProductDeleteEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class StatisticsListener {
@Order(1) // 控制执行顺序
@EventListener
public void recordDeleteStatistics(ProductDeleteEvent event) {
log.info("【统计模块】记录商品删除统计信息");
// 模拟统计逻辑
log.info("今日删除商品数+1");
log.info("商品分类'{}'删除计数+1", event.getProduct().getCategory());
if (event.isExpensiveProduct()) {
log.info("贵重商品删除统计+1,价格: {}", event.getProduct().getPrice());
}
}
@EventListener
public void sendNotification(ProductDeleteEvent event) {
log.info("【统计模块】发送商品删除通知");
log.info("通知运营人员: 商品{}已被{}删除,原因: {}",
event.getProduct().getName(),
event.getOperatorId(),
event.getDeleteReason());
}
}
4.2.5 商品服务类
java
package com.example.demo.service;
import com.example.demo.event.ProductEventPublisher;
import com.example.demo.model.Product;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductEventPublisher eventPublisher;
// 模拟商品数据库
private final ConcurrentHashMap<Long, Product> productDB = new ConcurrentHashMap<>();
public ProductService() {
// 初始化一些测试数据
productDB.put(1L, new Product(1L, "iPhone 13", "电子产品", new BigDecimal("6999"), 100));
productDB.put(2L, new Product(2L, "卫龙辣条", "食品", new BigDecimal("5"), 1000));
}
public void deleteProduct(Long productId, String deleteReason, Long operatorId) {
log.info("========== 开始删除商品 ==========");
// 1. 从数据库获取商品信息
Product product = productDB.get(productId);
if (product == null) {
log.error("商品不存在: {}", productId);
return;
}
// 2. 执行删除操作(实际项目中会删除数据库记录)
productDB.remove(productId);
log.info("商品删除成功: {}", product.getName());
// 3. 发布商品删除事件
eventPublisher.publishProductDeleteEvent(product, deleteReason, operatorId);
log.info("========== 商品删除流程完成 ==========\n");
}
}
4.2.6 启动类配置
java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync // 启用异步处理
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
}
测试控制器
java
package com.example.demo.controller;
import com.example.demo.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@DeleteMapping("/{productId}")
public String deleteProduct(
@PathVariable Long productId,
@RequestParam(defaultValue = "正常下架") String reason,
@RequestParam(defaultValue = "1") Long operatorId) {
productService.deleteProduct(productId, reason, operatorId);
return "商品删除请求已处理,请查看日志确认各模块响应";
}
}
运行结果
启动应用后,访问:http://localhost:8080/api/products/1?reason=过期下架&operatorId=1001
控制台输出:
========== 开始删除商品 ==========
商品删除成功: iPhone 13
2024-01-15 10:30:45.123 INFO 12345 --- [nio-8080-exec-1] c.e.d.event.ProductEventPublisher : 发布商品删除事件: 商品ID=1, 商品名称=iPhone 13, 删除原因=过期下架
========== 商品删除流程完成 ==========
2024-01-15 10:30:45.124 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.InventoryListener : 【库存模块】接收到商品删除事件
2024-01-15 10:30:45.124 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.InventoryListener : 开始清理库存数据: 商品ID=1, 商品名称=iPhone 13
2024-01-15 10:30:45.625 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.InventoryListener : 库存记录清理完成: 商品ID=1
2024-01-15 10:30:45.625 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.InventoryListener : 仓库空间释放完成
2024-01-15 10:30:45.626 INFO 12345 --- [ task-1] c.e.d.listener.OrderListener : 【订单模块】异步处理商品删除事件
2024-01-15 10:30:45.626 INFO 12345 --- [ task-1] c.e.d.listener.OrderListener : 开始处理相关订单: 商品ID=1
2024-01-15 10:30:46.627 INFO 12345 --- [ task-1] c.e.d.listener.OrderListener : 发现3个未完成订单,已通知客服处理
2024-01-15 10:30:46.628 INFO 12345 --- [ task-1] c.e.d.listener.OrderListener : 相关订单状态已更新为'商品已下架'
2024-01-15 10:30:46.629 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.StatisticsListener : 【统计模块】记录商品删除统计信息
2024-01-15 10:30:46.629 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.StatisticsListener : 今日删除商品数+1
2024-01-15 10:30:46.629 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.StatisticsListener : 商品分类'电子产品'删除计数+1
2024-01-15 10:30:46.629 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.StatisticsListener : 贵重商品删除统计+1,价格: 6999
2024-01-15 10:30:46.629 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.StatisticsListener : 【统计模块】发送商品删除通知
2024-01-15 10:30:46.630 INFO 12345 --- [nio-8080-exec-1] c.e.d.listener.StatisticsListener : 通知运营人员: 商品iPhone 13已被1001删除,原因: 过期下架
使用场景分析
典型应用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 数据同步 | 主数据变更后同步到各个子系统 | 商品删除后同步到搜索引擎、缓存 |
| 业务解耦 | 分离核心业务流程和辅助流程 | 订单完成后发送短信、邮件通知 |
| 审计日志 | 记录关键操作日志 | 用户登录、数据修改记录 |
| 缓存管理 | 数据变更后更新缓存 | 配置修改后刷新本地缓存 |
| 消息通知 | 触发各种类型的通知 | 系统告警、业务提醒 |
最佳实践建议
- 合理使用异步:耗时操作使用@Async
- 异常处理:监听器内部做好异常捕获
- 事务边界:结合@TransactionalEventListener控制事务
- 避免循环依赖:防止事件循环触发
- 监控告警:重要事件添加监控
高级特性
事务事件监听
java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(ProductDeleteEvent event) {
// 事务提交后执行
}
泛型事件支持
java
public class BaseEvent<T> extends ApplicationEvent {
private T data;
// ...
}
@EventListener
public void handleStringEvent(BaseEvent<String> event) {
// 处理String类型的事件
}