easy-event-bus 事件总线组件设计
本文档描述 easy-event-bus 组件的设计与实现。该组件提供领域事件发布/订阅完整链路,底层采用 SPI 分层架构:业务门面 → MessageProducer/Consumer 装饰器层 → MessageProducerImplementation/ConsumerImplementation SPI → easy-messaging Adapter → 实际 MQ 后端(RocketMQ / Redis / JDBC / ZeroMQ / Kafka,可选)。组件约 60% 为 Spring-free 纯 POJO,可在无容器环境下单独测试。
更名记录:组件由 easy-cqrs 更名为 easy-event-bus。框架采用 CQS 范式,本组件专注领域事件总线;原命令路由(
commands包)从未正式启用且不再发展,仅作为遗留代码保留。Java 包名io.matrix.cqrs与类名(CqrsMessagingAutoConfiguration等)保持不变,配置前缀不受影响。
一、背景与问题
1.1 为什么要独立事件总线层
Matrix Framework 已有 easy-messaging 提供 MQ 抽象,但业务层每次做事件通信都要:
| 痛点 | 现状 | 目标 |
|---|---|---|
| 事件语义缺失 | 只有 Message 通用 payload,业务层自己拼 headers | 领域事件有统一头(AGGREGATE_ID / AGGREGATE_TYPE / EVENT_TYPE) |
| 订阅处理样板 | 收到消息后业务层自己做 JSON 反序列化 + 路由 handler | DomainEventDispatcher 统一处理,业务层只需注册 handler |
| 后端绑定 | EventPublisher 硬编码 MQ 实现 | SPI 抽象后端,业务只写门面 |
| 消息拦截器缺失 | 日志/跟踪/幂等在业务层散落 | PrePostHandle / DuplicateDetection 装饰器链,框架统一管理 |
1.2 与 Axon / EventBus 的区别
| 维度 | Axon | Guava EventBus | Matrix easy-event-bus |
|---|---|---|---|
| 后端 | 绑定 Axon Server 或 EventStore | 进程内(反射) | 抽象 SPI(RocketMQ/Redis/JDBC 可替换) |
| 持久化 | 内置 EventStore | 无 | 不内置,用 easy-store |
| 消费语义 | 支持命令/事件两种总线 | 仅事件 | 领域事件总线(CQS 范式) |
| 依赖体积 | 大(Spring Boot + Axon Server) | 小 | 小(仅 easy-messaging + kernel) |
二、组件定位与边界
定位:后端中立的领域事件门面。业务声明 BaseDomainEvent 后,通过 DomainEventPublisher 发布,通过 DomainEventDispatcher 消费。后端 MQ 可替换,消费端支持幂等 + 拦截器链。
边界:
| 做 | 不做 |
|---|---|
| Event 发布 + 订阅 + 路由 handler | 不实现 MQ 后端(靠 easy-messaging) |
| 拦截器链(幂等 / PrePost) | 不做持久化/回放(属 Event Store 范畴) |
| SPI 抽象后端,Adapter 层桥接到 easy-messaging | 不做分布式事务(Saga / TCC 等) |
| DomainEventDispatcher 注册 handler + @PostConstruct 初始化 | 不做命令路由(commands 包为遗留代码,不再发展) |
约束:
MessageProducerImplementation/MessageConsumerImplementation是 SPI 契约,新后端只要实现这两个接口即可接入DomainEventDispatcher.initialize()标@PostConstruct——纯单元测试需手动调用hutool ClassUtil.loadClass硬依赖——事件反序列化阶段用它加载事件类(fallback 到 handler.getEventClass())- 所有 MQ 后端均为 optional scope——编译时不强制,运行时按需引入
三、总体架构
flowchart TD
subgraph "业务层"
A2["BaseDomainEvent (kernel-client 提供)"]
A3["业务 Service:publish(event)"]
end
subgraph "Facade 层"
B2["DomainEventPublisherImpl<br/>DomainEventPublisher"]
B3["DomainEventDispatcher<br/>initialize() + subscribe + handleMessage"]
end
subgraph "Decorator 层"
C1["MessageProducerImpl<br/>prepareHeaders + pre/post Interceptor + send"]
C2["MessageConsumerImpl<br/>ChannelMapping + DecoratedMessageHandlerFactory"]
C3["MessageHandlerDecoratorChainBuilder<br/>递归组装装饰链"]
C4["DecoratedMessageHandlerFactory<br/>按 order 排序 → Chain"]
end
subgraph "SPI 层 (后端中立)"
D1["MessageProducerImplementation<br/>接口:send() + setMessageIdIfNecessary + withContext"]
D2["MessageConsumerImplementation<br/>接口:subscribe + groupByChannel + getId + close"]
end
subgraph "Adapter 层 (easy-messaging 桥接)"
E1["EasyMessagingProducerAdapter<br/>ProducerImplementation → BackendProducer"]
E2["EasyMessagingConsumerAdapter<br/>ConsumerImplementation → BackendConsumer"]
end
subgraph "easy-messaging (compile scope 依赖)"
F1["BackendProducer"]
F2["BackendConsumer"]
end
subgraph "MQ 后端 (optional)"
G1["RocketMQ"]
G2["Redis"]
G3["JDBC"]
G4["ZeroMQ"]
G5["Kafka"]
end
A2 --> B2 --> C1 --> D1
C1 --> E1 --> F1
F1 --> G1
A3 --> B3 --> C2 --> D2
C2 --> E2 --> F2
F2 --> G2
B3 -- "@PostConstruct initialize()" --> C3 --> C4
关键设计:业务层只与 Facade 层(DomainEventPublisher / DomainEventDispatcher)交互;后端切换只需换 SPI 实现 + Adapter,业务零感知。
四、核心 API 草图
4.1 消息抽象
public interface Message {
String ID = "ID";
String PARTITION_ID = "PARTITION_ID";
String DESTINATION = "DESTINATION";
String DATE = "DATE";
String getId();
String getPayload();
Map<String, String> getHeaders();
String getHeader(String name);
String getRequiredHeader(String name); // 缺失抛 RuntimeException
boolean hasHeader(String name);
// ...
}
// MessageBuilder(链式 + 前缀 extraHeaders)
Message m = MessageBuilder.withPayload(body)
.withHeader("k", "v")
.withExtraHeaders("x-", extras) // 自动拼前缀
.build();
4.2 Producer SPI + Facade
// SPI:后端实现
public interface MessageProducerImplementation {
void send(Message message);
default void setMessageIdIfNecessary(Message message) {} // 默认 no-op
default void withContext(Runnable runnable) { runnable.run(); } // 默认直接跑
}
// Facade 装饰器:prepareHeaders + pre/post 拦截器 + send
public final class MessageProducerImpl implements MessageProducer {
// prepareHeaders: setMessageIdIfNecessary → DESTINATION (ChannelMapping) → PARTITION_ID (UUID if absent)
// preSend → implementation.send(message) → postSend(null) / postSend(exception)
// implementation.withContext(() -> send(message))
}
4.3 Event 发布/订阅
// 发布(Facade)
DomainEventPublisher publisher = ...;
publisher.publish(aggregateType, aggregateId, events, headers);
// 内部:序列化 BaseDomainEvent → Message(带 AGGREGATE_ID / AGGREGATE_TYPE / EVENT_TYPE)
// → MessageProducer.send(channel, message)
// 订阅(构建)
DomainEventHandlers handlers = DomainEventHandlersBuilder
.forAggregateType("com.example.Order")
.onEvent(OrderPaidEvent.class, envelope -> { ... })
.onEvent(OrderShippedEvent.class, envelope -> { ... })
.andForAggregateType("com.example.Invoice")
.onEvent(InvoiceCreatedEvent.class, envelope -> { ... })
.build();
// 初始化(@PostConstruct)
DomainEventDispatcher dispatcher = new DomainEventDispatcher("dispatcher-1", handlers, messageConsumer, nameMapping);
dispatcher.initialize(); // 生产由 Spring @PostConstruct 触发;纯单测需手动调
4.4 DomainEventDispatcher 处理流程
flowchart TD
A["MessageConsumer 收到 Message"]
B["handleMessage(msg, handlers)"]
C["externalEventTypeToEventClassName<br/>把外部 topic 事件名转回 Class.getName()"]
D{"遍历 handlers<br/>handler.accept(message) 匹配?"}
E["构造 handler 级幂等 key<br/>dispatcherId:aggregateType:eventName"]
F["detector.doWithMessage<br/>占位即标记(重复则跳过)"]
G["handle:ClassUtil.loadClass + JsonMapper.fromJson<br/>handler.invoke(envelope)"]
H["warn 日志记录 handlerKey 与 messageId<br/>继续后续 handler(异常隔离不重投)"]
I["处理下一个 handler"]
Z["结束(无匹配 handler)"]
A --> B --> C --> D
D -- "匹配" --> E --> F
F -- "首次消费" --> G
F -- "重复消息" --> I
G --> I
G -- "抛异常" --> H --> I
D -- "不匹配" --> I
I --> D
消费语义(v1.1.0 起):
- 异常隔离:单个 handler 异常只记 warn 日志,不影响同一消息的其他 handler,消息不重投
- handler 级幂等:幂等判定下沉到每个 handler 粒度(详见 §7.1)
五、SPI 分层架构
业务代码
↓
DomainEventPublisher / DomainEventDispatcher
↓
MessageProducerImpl / MessageConsumerImpl ← Decorator 层(Spring-free,可测)
↓ ↓
MessageProducerImplementation MessageConsumerImplementation ← SPI 接口(Spring-free,可测)
↓ ↓
EasyMessagingProducerAdapter EasyMessagingConsumerAdapter ← Adapter 层(桥接 easy-messaging)
↓ ↓
BackendProducer / BackendConsumer ← easy-messaging(compile scope)
↓
RocketMQ / Redis / JDBC / ZeroMQ / Kafka ← MQ 后端(optional scope)
为什么分层?
- Decorator 层与 SPI 层:
MessageProducerImpl.send()包含 prepareHeaders / ChannelMapping / PartitionId 自动生成 / 拦截器 pre/post。这些逻辑和具体 MQ 实现无关,放在 Decorator 层让后端实现只关心”怎么把 Message 发出去”。 - Adapter 层:easy-messaging 提供
BackendProducer/SendMessageAPI,Adapter 把MessageProducerImplementation转成BackendProducer。业务层看不到 Adapter 的存在——CqrsMessagingAutoConfiguration(类名为历史命名,保持不变)在启动时自动装配。 - Optional scope 后端:MQ 后端(RocketMQ / Redis / JDBC / ZeroMQ / Kafka)全部
optional scope,编译时不强制,运行时按需引入 +easy.messaging.backend=rocketmq选后端。
六、包结构与核心类
easy-event-bus/
└── io/matrix/cqrs/ # 包名保持不变(历史命名)
├── core/ # ✅ Spring-free,纯 POJO/SPI,可单独测
│ ├── Message.java # 接口:ID/PARTITION_ID/DESTINATION/DATE
│ ├── MessageImpl.java # 实现:payload + headers Map
│ ├── MessageInterceptor.java # 接口:preSend/postSend + preReceive/postReceive
│ ├── ChannelMapping.java # 接口:transform(channel) → String
│ ├── DefaultChannelMapping.java # Builder 实现
│ ├── SubscriberIdAndMessage.java # 组合:subscriberId + Message
│ ├── producer/
│ │ ├── MessageBuilder.java # 链式构建:withPayload / withHeader / withExtraHeaders / build
│ │ ├── MessageProducer.java # 接口:send(destination, Message)
│ │ ├── MessageProducerImpl.java # 装饰器:prepareHeaders + pre/post + withContext
│ │ └── MessageProducerImplementation.java # SPI:send + setMessageIdIfNecessary + withContext
│ └── consumer/
│ ├── MessageHandler.java # extends Consumer<Message>
│ ├── MessageConsumer.java # 接口:subscribe + groupByChannel + close
│ ├── MessageConsumerImpl.java # 装饰器:ChannelMapping + DecoratedMessageHandlerFactory
│ ├── MessageConsumerImplementation.java # SPI
│ ├── DecoratedMessageHandlerFactory.java # 按 order 排序 → ChainBuilder 递归组装
│ ├── MessageHandlerDecorator.java # extends BiConsumer<SubscriberIdAndMessage, Chain> + getOrder()
│ ├── MessageHandlerDecoratorChain.java # 接口:invokeNext(SubscriberIdAndMessage)
│ ├── MessageHandlerDecoratorChainBuilder.java # 递归组装(private,已合并到 Factory)
│ ├── DuplicateMessageDetector.java # SPI:isDuplicate(consumerId, messageId) + doWithMessage
│ ├── NoopDuplicateMessageDetector.java # 默认实现(幂等默认关闭):恒 false + 直调 callback
│ ├── DuplicateMessageHandlerDecorator.java # 消息级去重装饰器(保留,不再默认装配)
│ ├── PrePostHandleMessageHandlerDecorator.java
│ ├── PrePostReceiveMessageHandlerDecorator.java
│ ├── BuiltInMessageHandlerDecoratorOrder.java # 内置 order 常量
│ ├── ConsumeException.java
│ └── MessageSubscription.java
├── commands/ # ⚠️ 遗留代码:命令路由从未正式启用,不再发展,勿基于它做新功能
├── events/
│ ├── EventMessageHeaders.java # AGGREGATE_ID / AGGREGATE_TYPE / EVENT_TYPE
│ ├── DomainEventNameMapping.java / DefaultDomainEventNameMapping.java
│ ├── publisher/
│ │ ├── DomainEventPublisher.java / DomainEventPublisherImpl.java
│ │ └── ResultWithEvents.java # 值对象:result + List<BaseDomainEvent>
│ └── subscriber/
│ ├── DomainEventHandler.java # 三元组:aggregateType + eventClass + handler
│ ├── DomainEventHandlers.java # 聚合 List<DomainEventHandler>
│ ├── DomainEventHandlersBuilder.java # Builder:forAggregateType / onEvent / andForAggregateType
│ ├── DomainEventEnvelope.java / DomainEventEnvelopeImpl.java
│ ├── DomainEventDispatcher.java # @PostConstruct initialize() + handleMessage(异常隔离 + handler 级幂等)
│ ├── DomainEventDispatcherFactory.java / DispatcherFactoryBean.java
│ └── BaseEventConsumer.java
├── adapter/ # Bridge:easy-messaging ↔ SPI
│ ├── EasyMessagingProducerAdapter.java
│ ├── EasyMessagingConsumerAdapter.java
│ └── ChannelProperties.java
└── autoconfigure/
├── CqrsChannelsProperties.java # 类名保持不变(历史命名)
└── CqrsMessagingAutoConfiguration.java
| 层 | 关键类 | Spring-free | 已测试 |
|---|---|---|---|
| core.producer | Message / MessageImpl / MessageBuilder / MessageProducerImpl / MessageProducerImplementation | ✅ | ✅ 32 例 |
| core.consumer | MessageConsumerImpl / NoopDuplicateMessageDetector / DefaultChannelMapping | ✅ | ✅ |
| events.subscriber | DomainEventHandler / DomainEventHandlers / DomainEventHandlersBuilder | ✅ | 可补 |
| events.publisher | DomainEventPublisherImpl / ResultWithEvents | ✅ | ✅ |
| adapter | EasyMessagingProducerAdapter / EasyMessagingConsumerAdapter | ❌ 依赖 easy-messaging Backend | 跳过 |
| autoconfig | CqrsMessagingAutoConfiguration | ❌ Spring Boot @Configuration | 跳过 |
七、消息装饰链
MessageHandlerDecoratorChainBuilder 递归组装装饰器责任链:
// 伪代码(核心逻辑)
private MessageHandlerDecoratorChain buildChain(List<MessageHandlerDecorator> handlers, Consumer<SubscriberIdAndMessage> terminal) {
if (handlers.isEmpty()) {
return terminal::accept; // 最终 handler
}
MessageHandlerDecorator head = handlers.get(0);
List<MessageHandlerDecorator> tail = handlers.subList(1, handlers.size());
return subscriberIdAndMessage -> head.accept(subscriberIdAndMessage, buildChain(tail, terminal));
}
默认装配的装饰器按 order 排序:
| Order | 装饰器 | 作用 |
|---|---|---|
| 中 | PrePostReceiveMessageHandlerDecorator | MessageInterceptor.preReceive / postReceive |
| 高 | PrePostHandleMessageHandlerDecorator | MessageInterceptor.preHandle / postHandle |
| 最高 | 业务 Handler (terminal) | 实际 DomainEventDispatcher.handleMessage |
DuplicateMessageHandlerDecorator(消息级去重装饰器)类保留但不再默认装配:一个事件会被多个 handler 消费,消息级全局去重会误杀第 2..N 个 handler。幂等改为 handler 级,由 DomainEventDispatcher 直接与 DuplicateMessageDetector 协作(见 §7.1)。
7.1 消费语义:异常隔离 + handler 级幂等(v1.1.0)
一个事件会被多个业务场景(handler)消费,v1.1.0 之前 DomainEventDispatcher.handleMessage 用 stream().forEach 直调 handler,存在两个可靠性缺口:
| 缺口 | 后果 |
|---|---|
| 无 try-catch,handler A 抛异常中断 forEach | Handler B..N 全部不执行;且非 ConsumeException 被 Adapter 吞掉后 MQ 端 ack 正常、消息不重投——B..N 的处理永久丢失 |
| Noop 幂等 + 消息级装饰器粒度 | 多 handler 场景若按 messageId 全局去重,会误杀第 2..N 个 handler |
设计上不改 DuplicateMessageDetector 接口,粒度下沉通过 dispatcher 内为每个 handler 构造新的 SubscriberIdAndMessage 实现:
for (DomainEventHandler handler : handlers.getHandlers()) {
if (!handler.accept(message)) { continue; }
String handlerKey = handler.getAggregateType() + ":" + handler.getEventClass().getSimpleName();
try {
duplicateMessageDetector.doWithMessage(
new SubscriberIdAndMessage(eventDispatcherId + ":" + handlerKey, message),
() -> handle(message, aggregateType, handler));
} catch (Exception ex) {
logger.warn("事件 handler 处理失败,跳过并继续后续 handler: handlerKey={}, messageId={}",
handlerKey, message.getId(), ex);
}
}
设计要点:
- 幂等 key:
{dispatcherId}:{aggregateType}:{eventSimpleName}+ messageId——同一事件被多个 handler 消费时互不误杀 - 异常隔离:per-handler try-catch,单 handler 失败仅记 warn 日志(含 handlerKey 与 messageId),继续执行后续 handler;消息不重投,失败补偿依赖日志告警
- 复用 SPI:Dispatcher 不感知幂等实现,Noop / 业务自定义均透明接入
幂等默认关闭:幂等是业务语义(唯一约束/状态机检查等可靠手段在业务层),框架不引入额外中间件(如 Redis)做默认实现,只保证消费链异常隔离 + SPI 挂载点。默认装配 NoopDuplicateMessageDetector(@ConditionalOnMissingBean,业务实现 SPI 注册 Bean 即覆盖,按 handler 级粒度生效)。
八、配置与后端
8.1 MQ 后端选择
| 后端 | Maven 坐标 | scope | 启用条件 |
|---|---|---|---|
| RocketMQ | org.apache.rocketmq:rocketmq-client:4.9.7 | optional | easy.messaging.backend=rocketmq |
| Redis | spring-boot-starter-data-redis | optional | easy.messaging.backend=redis |
| JDBC | spring-boot-starter-jdbc | optional | easy.messaging.backend=jdbc |
| ZeroMQ | org.zeromq:jeromq:0.5.3 | optional | easy.messaging.backend=zeromq |
| Kafka | org.apache.kafka:kafka-clients:3.4.1 | optional | easy.messaging.backend=kafka |
注意:easy-event-bus 不直接创建后端——它通过 @AutoConfigureAfter(MessagingAutoConfiguration.class) 依赖 easy-messaging 的 BackendProducer/BackendConsumer Bean。后端切换由 easy-messaging 配置驱动。
8.2 SPI 可扩展点
| SPI | 默认实现 | 扩展方式 |
|---|---|---|
| MessageProducerImplementation | 由 EasyMessagingProducerAdapter 桥接 easy-messaging BackendProducer | 自建后端直接实现 SPI 接口 |
| MessageConsumerImplementation | 由 EasyMessagingConsumerAdapter 桥接 easy-messaging BackendConsumer | 同上 |
| ChannelMapping | DefaultChannelMapping(恒等 transform) | @Bean ChannelMapping 自定义映射规则 |
| DomainEventNameMapping | DefaultDomainEventNameMapping(eventType = event.getClass().getName()) | @Bean DomainEventNameMapping 自定义映射 |
| DuplicateMessageDetector | NoopDuplicateMessageDetector(幂等默认关闭) | 实现 SPI + @Bean 即启用(幂等为 handler 级粒度,见 §7.1) |
九、使用示例
9.1 发布领域事件
// 1. 定义事件(extends BaseDomainEvent)
public class OrderPaidEvent extends BaseDomainEvent {
private String orderId;
private BigDecimal amount;
}
// 2. 聚合根发出 ResultWithEvents
public class OrderAggregate extends AggregateRoot {
public ResultWithEvents<Order> pay() {
// 业务逻辑
OrderPaidEvent evt = new OrderPaidEvent(...);
return new ResultWithEvents<>(this, evt);
}
}
// 3. Application Service 发布
ResultWithEvents<Order> rwe = order.pay();
rwe.events.forEach(e ->
domainEventPublisher.publish("com.example.Order", order.getId(), e, Collections.emptyMap())
);
9.2 订阅领域事件
// Spring Bean
@Component
public class OrderEventHandlerConfig {
public OrderEventHandlerConfig(DomainEventDispatcherFactory factory) {
DomainEventHandlers handlers = DomainEventHandlersBuilder
.forAggregateType("com.example.Order")
.onEvent(OrderPaidEvent.class, envelope -> {
// envelope.getAggregateId() / envelope.getEvent() / envelope.getAggregateType()
invoiceService.create(envelope.getEvent());
})
.onEvent(OrderShippedEvent.class, envelope -> {
notificationService.send(envelope);
})
.andForAggregateType("com.example.Invoice")
.onEvent(InvoiceCreatedEvent.class, envelope -> { ... })
.build();
factory.make("order-event-dispatcher", handlers); // 启动时 @PostConstruct initialize()
}
}
9.3 纯 SPI 后端(不依赖 easy-messaging)
// 自定义 ZeroMQ 后端,直接实现 SPI
public class ZeroMqProducerImpl implements MessageProducerImplementation {
@Override public void send(Message message) { /* ZeroMQ publish */ }
}
// 业务层注入替换
@Configuration
public class ZeroMqConfig {
@Bean public MessageProducerImplementation zeroMqProducer() {
return new ZeroMqProducerImpl();
}
}
// MessageProducerImpl 构造时自动注入新的 Implementation
十、关键设计决策
| 决策 | 内容 | 理由 |
|---|---|---|
| 组件更名 easy-cqrs → easy-event-bus | 文档与坐标改名;包名 io.matrix.cqrs 与类名不变 | 框架走 CQS 范式,命令路由不启用,组件本质是事件总线;避免破坏消费方 import 与配置 |
| Producer/Consumer 分层 Decorator → SPI → Adapter | Decorator 层做通用逻辑(prepareHeaders / ChannelMapping / 拦截器链),SPI 层只关心”怎么发消息” | 后端切换零成本;Decorator 层可单独测 |
| ChannelMapping 默认恒等 transform | 不做 virtual-dest → real-topic 映射则直传 | 零配置启动;业务需要时加 @Bean ChannelMapping |
| PartitionId 自动 UUID 生成 | prepareMessageHeaders 检测到 PARTITION_ID header 为空才生成 | 业务可在 MessageBuilder 阶段手动指定(保持跨后端路由一致性),框架兜底保证不为 null |
| DomainEventDispatcher.initialize() 在 @PostConstruct | 启动时一次性 groupByChannel + subscribe,避免运行时动态注册复杂性 | 纯单测需要手动调 initialize() |
| DomainEventNameMapping 两步转换 | 外部 topic event-type → 内部 Class.getName()(externalEventTypeToEventClassName) | 序列化时用内部类名(DefaultDomainEventNameMapping),反序列化时转回 |
| ClassUtil.loadClass fallback | ClassUtil 加载失败用 handler.getEventClass() | 支持事件类不在本应用 ClassPath 场景(如跨服务消费) |
| NoopDuplicateMessageDetector 默认 | isDuplicate 恒 false,幂等默认关闭 | 幂等是业务语义(唯一约束/状态机等可靠手段在业务层),框架不引入额外中间件做默认实现,也不强制幂等 |
| handler 级幂等 + 异常隔离(v1.1.0) | 幂等 key = dispatcherId:aggregateType:eventName + messageId,DomainEventDispatcher 内 per-handler doWithMessage + try-catch;消息级 DuplicateMessageHandlerDecorator 不再默认装配 | 一个事件多 handler 消费:消息级全局去重会误杀第 2..N 个 handler;无隔离时单 handler 异常导致其余处理永久丢失(消息不重投) |
| MessageBuilder headers 共享引用 | build() 产出的 Message 和 Builder 共享 headers Map | 业务层后续 modify Builder 会影响已 build 的 Message——文档需要注明 |
| 所有 MQ 后端 optional scope | 编译时不强依赖任何 MQ 客户端 | easy-event-bus 核心层可单独编译;运行时按需引入 |
十一、测试覆盖
当前共 34 例(全绿),全部为纯 JVM 测试(零 MQ / 零 Spring 容器):
| 测试类 | 用例 | 覆盖点 |
|---|---|---|
| MessageImplTest | 11 | payload/headers CRUD、getRequiredHeader 边界(null → RuntimeException)、setHeader 懒初始化 headers map、getId |
| MessageBuilderTest | 6 | withPayload / withMessage 拷贝 / withHeader / withExtraHeaders 前缀拼接 / null headers 空值 / build 与 Builder 共享 headers 引用 |
| DefaultChannelMappingTest | 4 | Builder 链式构建、恒等 transform、显式映射 transform、空映射恒等 |
| MessageProducerImplTest | 6 | prepareMessageHeaders(DESTINATION 自动注入 / PARTITION_ID UUID 兜底 / 已存在保留)、ChannelMapping 转换、Interceptor pre/post 顺序、异常传播 + postSend 异常 |
| NoopDuplicateMessageDetectorTest | 2 | isDuplicate 恒 false、doWithMessage 直调 callback |
| ResultWithEventsTest | 3 | 两参/varargs 构造器、null events |
| DomainEventDispatcherIsolationTest | 2 | 单 handler 异常不阻断后续 handler(异常隔离)、detector 判定重复时 handler 不执行(handler 级幂等) |
可测程度 2/5(核心层 Spring-free 约 60%,Adapter/AutoConfiguration 属 Spring 集成测试范畴):
- ✅ 已覆盖:Message/MessageBuilder/MessageProducerImpl/DefaultChannelMapping/NoopDuplicateMessageDetector/DomainEventDispatcher(异常隔离 + handler 级幂等)/ResultWithEvents(共 34 例)
- ❌ 未覆盖:DomainEventHandlersBuilder(纯 Builder,可补)、EasyMessagingProducerAdapter/EasyMessagingConsumerAdapter(需 easy-messaging BackendProducer mock)、CqrsMessagingAutoConfiguration(需 Spring Boot 测试容器)