事件系统
为何解耦
业务完成一个动作后,往往还要触发多个下游副作用,且这些副作用不应阻塞主流程、也不应让发布方直接依赖各消费模块的实现类。
典型场景:
| 场景 | 发布方 | 消费方(示例) |
|---|---|---|
| 商品新增 | ecshopx-goods | 有数同步、营销中心推送 |
| 订单流程日志 | ecshopx-orders | 日志落库、ERP 同步 |
| 微信发货通知 | ecshopx-orders | 微信侧发货状态上报 |
若 Service 里直接 import 并调用各模块 Service,会形成紧耦合与循环依赖。ECShopX Java 的跨模块解耦主路径是 ecshopx-dispatch 事件总线(见下文);模块内部的 Spring 事件作为补充手段。
两条路径(先分清)
| 机制 | 适用 | 入口 |
|---|---|---|
| Dispatch 事件总线 | 跨 Bundle、需与已有事件名常量对齐 | DispatchFacade#publishEvent |
| Spring 应用事件 | 单模块内、事务边界清晰 | ApplicationEventPublisher#publishEvent + @TransactionalEventListener |
二次开发跨模块协作时,优先使用 Dispatch 总线。Spring 事件示例见 ecshopx-orders/.../listener/OrderProcessLogSpringEventListener(@TransactionalEventListener(phase = AFTER_COMMIT))。
事件名与 payload 放哪里
事件名常量
跨模块共享的事件名字符串集中在 ecshopx-common 的 *DispatchEventNames 类,例如:
// ecshopx-common/.../GoodsDispatchEventNames.java
public static final String EVENT_ITEM_ADD = "event:271:GoodsBundle\\Events\\ItemAddEvent";各域还有 OrdersDispatchEventNames、AdaPayDispatchJobNames 等,按业务域查找即可。
模块内目录
| 目录 | 职责 |
|---|---|
dispatch | 发布辅助接口(Port 实现类在 ecshopx-bootstrap/config)、消费方 DispatchListener / DispatchHandler |
event | 领域 Spring 事件 POJO(若模块有定义);部分模块当前为占位 |
发布方通常在业务模块定义 XxxEventDispatchPublisher 接口,在 ecshopx-bootstrap 的 config 包提供实现并注入 DispatchFacade。
发布事件(Dispatch 总线)
最小流程
- 在
ecshopx-common(或已有*DispatchEventNames)声明messageName常量。 - 在发布模块的
dispatch包定义XxxEventDispatchPublisher接口。 - 在
ecshopx-bootstrap/config实现该接口,调用DispatchFacade#publishEvent。 - 业务 Service 注入 Publisher,在事务提交后发布(见下方 afterCommit 模式)。
真实示例:商品新增
ItemAddEventDispatchPublisherImpl(bootstrap)在事务提交后异步发布:
dispatchFacade.publishEvent(
GoodsDispatchEventNames.EVENT_ITEM_ADD,
payload,
new DispatchOptions(
DispatchMode.ASYNC,
DispatchDriverType.REDIS,
null,
null,
RetryPolicy.platformDefault()));payload 为 Map<String, Object>,本例包含 item_id、company_id。
afterCommit 模式
写库与发事件应分离:在活跃事务中注册 TransactionSynchronization#afterCommit,避免监听器读到未提交数据。ItemAddEventDispatchPublisherImpl、OrderProcessLogPublishPortImpl 均采用此模式。
订单流程日志发布(OrderProcessLogPublishPortImpl):
dispatchFacade.publishEvent(
OrdersDispatchEventNames.EVENT_ORDER_PROCESS_LOG,
payload,
DispatchOptions.oplQueuedAfterCommit());注册监听 / 消费
Dispatch 总线不使用 Spring 的 @EventListener 做跨模块注册,而是通过 DispatchRegistry 显式绑定。
1. 实现 DispatchListener
消费方在自身模块 dispatch 包实现接口:
@Component
public class ItemAddYoushuDispatchListener implements DispatchListener {
@Override
public void onEvent(Map<String, Object> payload) {
// 解析 payload,执行业务
}
}2. 在 bootstrap 注册
ecshopx-bootstrap/config/*DispatchListenerRegistrationConfig 在 @PostConstruct 中注册:
dispatchRegistry.registerEventListener(
GoodsDispatchEventNames.EVENT_ITEM_ADD,
"listener:youshu.items_item_add",
ListenerDispatchOptions.async("default", null),
itemAddYoushuDispatchListener);参数说明:
| 参数 | 含义 |
|---|---|
messageName | 与发布方 publishEvent 第一个参数一致 |
listenerName | 全局唯一标识,fan-out 时写入消息体 |
ListenerDispatchOptions | 该监听器的队列、延迟、重试策略 |
DispatchListener | 消费实现 |
同一 messageName 可注册多个 listener;异步模式下 DispatchFanOutPlanner 会为每个 listener 生成独立队列消息。
3. 运行时消费
- 同步(
DispatchMode.SYNC):SyncDispatchDriver在当前线程依次调用匹配的 listener。 - 异步(
DispatchMode.ASYNC+DispatchDriverType.REDIS):消息入 Redis 队列,由 consumer 调用DispatchConsumerRuntime#consume执行 listener。
同步 / 异步、延迟、重试、失败落库的完整说明见 异步与定时任务。
同步 vs 异步:如何选择
| 模式 | 典型用途 | DispatchOptions |
|---|---|---|
| 同步 | 必须在本请求内完成、listener 极少且快速 | DispatchOptions.eventDefaults()(DispatchMode.SYNC) |
| 异步 | 跨模块 fan-out、可容忍秒级延迟、需重试 | DispatchMode.ASYNC + DispatchDriverType.REDIS |
原则:
- 跨模块、多订阅者 → 异步 + Redis(默认主路径)。
- 单 listener 且逻辑极轻 → 可考虑同步;多 listener 时同步路径行为与 fan-out 不同,见
DispatchFacade#publishEvent源码。 - 需要延迟投递、失败重试、死信 → 必须走异步,详见 async-jobs.md。
二次开发检查清单
- [ ] 事件名是否与已有
*DispatchEventNames对齐(避免重复造字符串) - [ ] 发布是否在 afterCommit 之后(有写库时)
- [ ] 消费方是否实现
DispatchListener并在 bootstrap*RegistrationConfig注册 - [ ]
listenerName是否全局唯一、便于排查 - [ ] 异步 listener 是否配置合适的
queue/delay/RetryPolicy - [ ] 是否在 XXL-JOB 与 Dispatch Job 之间选对机制(定时扫表用 cron,业务触发的后台任务用
dispatchJob,见 async-jobs.md)
