Handler
Command、Query、DomainEvent 都只是装着参数的对象,真正干活的是 Handler(处理器)。三类消息对应三类 Handler:
| 消息 | Handler 接口 | 方法签名 | 每个消息几个 |
|---|---|---|---|
Command | CommandHandler<C, R> | R handle(C command) | 恰好 1 个 |
Query | QueryHandler<Q, R> | R handle(Q query) | 恰好 1 个 |
DomainEvent | EventHandler<T> | void handle(T event) | 0 到 n 个 |
类名跟着消息走:CreateProducerCommand → CreateProducerCommandHandler,GetProducerQuery → GetProducerQueryHandler,ProducerEvent → ProducerEventHandler。
Handler 往下调什么
Handler 住在应用层,它本身不实现业务,只做编排。往下是两样东西:
| Handler | 调聚合根 | 调 Repository | 返回值 |
|---|---|---|---|
CommandHandler | 是 —— 业务规则在聚合根里 | 写仓储(save / update / delete / operate) | 有 |
QueryHandler | 一般不用 | 读仓储 | 有 |
EventHandler | 不改原来的聚合 | 更新读模型时才用;不发新命令时可以什么都不调 | 无 |
一句话:Handler 是执行者,聚合根是规则,Repository 是数据存取的边界。 消息 → Handler → 聚合根 / Repository,这条链就是 soda 的骨架。
三种 Handler 一样的地方
- 都是普通 Spring Bean。 加
@Component就是全部注册工作 —— 没有subscribe、没有注册表、没有 XML。 - 只有一个
handle方法。 参数就是消息本身,失败统一抛BaseException。 - 处理哪种消息由泛型决定。 总线启动时注入
List<CommandHandler<?, ?>>这样的集合,用GenericTypeResolver.resolveTypeArguments(...)从泛型里读出消息类型,建索引。 - 放在应用层:
application/cqrs/handler/{command,query,event}。 - 都被
CqrsAroundHandler切面拦。 切面负责把上下文塞进 ThreadLocal、记耗时和入参出参、finally里清理。
@Component
@RequiredArgsConstructor
public class CreateProducerCommandHandler
implements CommandHandler<CreateProducerCommand, ProducerDTO> { // ← 泛型决定"我处理哪个命令"
private final ProducerWriteRepository producerWriteRepository;
@Override
public ProducerDTO handle(CreateProducerCommand command) throws BaseException {
...
}
}总线怎么找到 Handler
Command ──→ CommandBus ──→ Map<Class, CommandHandler> 按精确类找,1 个
Query ──→ QueryBus ──→ Map<Class, QueryHandler> 按精确类找,1 个
DomainEvent ──→ EventBus ──→ Map<Class, List<EventHandler>> 自身 + 父类 + 接口,0..n 个命令和查询按消息的精确类查 —— 给 GetProducerQuery 注册了 Handler,发一个它的子类,是找不到的。事件不一样,从消息类开始沿父类链和实现的接口往上走,所以注册在 DomainEvent 上的 Handler 能收到所有事件。
CommandHandler:改状态
CommandHandler 是写操作的执行者,一个命令只能有一个:
@Component
@RequiredArgsConstructor
public class CreateProducerCommandHandler implements CommandHandler<CreateProducerCommand, ProducerDTO> {
private final ProducerWriteRepository producerWriteRepository;
@Override
public ProducerDTO handle(CreateProducerCommand command) throws BaseException {
commandValidate(command); // 1. 校验
Producer domain = ProducerAssembler.toDomain(command); // 2. 组装聚合根
domain.create(command.getName()); // 3. 改状态,挂起事件
producerWriteRepository.save(domain); // 4. 存仓储(切面在这里发事件)
return ProducerWebAssembler.toDTO(domain);
}
private void commandValidate(CreateProducerCommand command) throws BaseException {
ParamChecker.notNull(command, "command");
ParamChecker.notNull(command.getName(), "name");
}
}- 有返回值。
CommandHandler<C, R>的R就是handle的返回类型。 - 失败往上抛。 抛
BaseException一路往上走;外层有事务(你自己开的@Transactional)就回滚,挂起的事件不会被切面取走。 - 业务规则不写在这里。 Handler 只负责把消息翻译成聚合根的调用,规则属于聚合根。
- 注入接口,不注入实现类。
ProducerWriteRepository而不是ProducerWriteRepositoryImpl。
两个 Handler 注册同一个命令不会报错,后注册的静默覆盖前面的。启动日志里只会看到两行 Registered handler for command: ...,看不出来被覆盖了。一个命令的类型只应该有一个 Handler。
QueryHandler:读状态
形状和 CommandHandler 几乎一样,区别在做什么:
@Component
@RequiredArgsConstructor
public class GetProducerQueryHandler implements QueryHandler<GetProducerQuery, Producer> {
private final ProducerReadRepository producerReadRepository;
@Override
public Producer handle(GetProducerQuery query) throws BaseException {
queryValidate(query);
return producerReadRepository.getByUid(query.getUid());
}
}- 一个查询只有一个 Handler,同样是后注册覆盖。
- 只读。 不要在这里调写仓储 —— 读链路一旦触发写操作,CQRS 就退化回普通分层。
- 返回领域对象或 DTO 都行,取决于你在哪一层组装。写一个查询 里的两个例子各用了一种。
EventHandler:状态变了之后
EventHandler 不做"响应",只做"善后" —— 状态已经在别处改完了,它收到通知去处理后续动作:
@Component
@RequiredArgsConstructor
public class ProducerEventHandler implements EventHandler<ProducerEvent> {
@Override
public void handle(ProducerEvent event) throws BaseException {
ParamChecker.notNull(event.getProducer(), "producer");
// 发消息、发通知、更新读模型……
}
}- 0 到 n 个,没人处理不报错。 发出去没有 EventHandler 接收,静默跳过。
- 没有返回值。
void handle(T event),结果只能通过副作用体现。 - 执行顺序不要依赖。 同一事件的多个 Handler 按 Bean 注入顺序调用,顺序不是接口约定。
- 一个类上的 Handler 抛异常,同一事件剩下的 Handler 不会执行。
SimpleEventBus是顺序调用,没有逐个 try/catch。 - 失败不回滚。 事件在事务提交之后才发布,
RepositoryEventAspect只能把异常记进日志。Redis 总线例外 —— 它按max-retries重试,最终失败进死信流。 - 可以按
eventType分支,也可以拆成多个事件类。 后者更清楚,见 领域事件。 - 可以在这里发新命令。
busFacade.sendCommand(...)会把上下文接上,注意递归保护。
写 Handler 常踩的坑
| 现象 | 原因 |
|---|---|
No handler registered for command: ... | Handler 没加 @Component,或者不在组件扫描范围内 |
| 命令发出去找不到 Handler,类名明明对 | 泛型写成了父类(如 CommandHandler<BaseCommand<X>, X>),索引建在了父类上 |
| EventHandler 永远不触发 | 事件类的泛型写错;或者 bus-type 配成了 spring,根本没有 EventBus Bean |
| 改了一个 Handler,行为却没变 | 同一命令/查询有第二个 Handler,后者覆盖了它 |
启动时看不到 Registered handler 日志 | @Component 没生效,或者包不在扫描路径里 |
名字里带 Handler,但不是消息 Handler
工程里还有几个类以 Handler 结尾,它们和上面的三类 Handler 没有关系:
| 类 | 是什么 | 干什么 | 来自哪里 |
|---|---|---|---|
ControllerAroundHandler | Web 层切面 | 从请求头取 Soda-Request-Id / 用户名,填进命令、写 MDC | 生成工程的 adapter 层 |
CqrsAroundHandler | CQRS 切面 | 拦截三类 Handler 的 handle 和 CommandBus.send,搬上下文、记日志、防递归 | soda-core 框架 |
GlobalExceptionHandler | @RestControllerAdvice | 把 BaseException 映射成 Result.fail(code, message) 作兜底 | 生成工程的 adapter 层 |
它们不处理命令、查询、事件,只做横切。看到 Handler 后缀先别急着归到三类消息 Handler 里。