Skip to content

事件系统 ​

为何解耦 ​

业务完成一个动作后,往往还要触发多个下游副作用,且这些副作用不应阻塞主流程、也不应让发布方直接依赖各消费模块的实现类。

典型场景:

场景发布方消费方(示例)
商品新增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 类,例如:

java
// 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 总线) ​

最小流程 ​

  1. 在 ecshopx-common(或已有 *DispatchEventNames)声明 messageName 常量。
  2. 在发布模块的 dispatch 包定义 XxxEventDispatchPublisher 接口。
  3. 在 ecshopx-bootstrap/config 实现该接口,调用 DispatchFacade#publishEvent。
  4. 业务 Service 注入 Publisher,在事务提交后发布(见下方 afterCommit 模式)。

真实示例:商品新增 ​

ItemAddEventDispatchPublisherImpl(bootstrap)在事务提交后异步发布:

java
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):

java
dispatchFacade.publishEvent(
        OrdersDispatchEventNames.EVENT_ORDER_PROCESS_LOG,
        payload,
        DispatchOptions.oplQueuedAfterCommit());

注册监听 / 消费 ​

Dispatch 总线不使用 Spring 的 @EventListener 做跨模块注册,而是通过 DispatchRegistry 显式绑定。

1. 实现 DispatchListener ​

消费方在自身模块 dispatch 包实现接口:

java
@Component
public class ItemAddYoushuDispatchListener implements DispatchListener {
    @Override
    public void onEvent(Map<String, Object> payload) {
        // 解析 payload,执行业务
    }
}

2. 在 bootstrap 注册 ​

ecshopx-bootstrap/config/*DispatchListenerRegistrationConfig 在 @PostConstruct 中注册:

java
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)

相关章节 ​