soda docs v17.0.1

Handler

Command、Query、DomainEvent 都只是装着参数的对象,真正干活的是 Handler(处理器)。三类消息对应三类 Handler:

消息Handler 接口方法签名每个消息几个
CommandCommandHandler<C, R>R handle(C command)恰好 1 个
QueryQueryHandler<Q, R>R handle(Q query)恰好 1 个
DomainEventEventHandler<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 里清理。
java
@Component
@RequiredArgsConstructor
public class CreateProducerCommandHandler
        implements CommandHandler<CreateProducerCommand, ProducerDTO> {   // ← 泛型决定"我处理哪个命令"
    private final ProducerWriteRepository producerWriteRepository;

    @Override
    public ProducerDTO handle(CreateProducerCommand command) throws BaseException {
        ...
    }
}

总线怎么找到 Handler

text
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 是写操作的执行者,一个命令只能有一个:

java
@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 几乎一样,区别在做什么:

java
@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 不做"响应",只做"善后" —— 状态已经在别处改完了,它收到通知去处理后续动作:

java
@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 没有关系:

类是什么干什么来自哪里
ControllerAroundHandlerWeb 层切面从请求头取 Soda-Request-Id / 用户名,填进命令、写 MDC生成工程的 adapter 层
CqrsAroundHandlerCQRS 切面拦截三类 Handler 的 handle 和 CommandBus.send,搬上下文、记日志、防递归soda-core 框架
GlobalExceptionHandler@RestControllerAdvice把 BaseException 映射成 Result.fail(code, message) 作兜底生成工程的 adapter 层

它们不处理命令、查询、事件,只做横切。看到 Handler 后缀先别急着归到三类消息 Handler 里。

下一步