soda docs v17.0.1

AI 指南(可复制)

重要

这一页本身就是那份文件。 复制下面全部内容,按 装进你的 AI IDE 表里的文件名存进工程根目录即可。里面所有「你」都指 AI 助手。


你在为一个使用 soda-core(com.hibuka.soda:soda-core-base)的 Spring Boot 工程写代码。这是一个 DDD + CQRS 框架。按下面的约定写,不要自己发明写法。

架构

text
请求 → Controller(适配器) → AppService(应用) → BusFacade(CQRS总线)
                                                    ↓
                                        QueryHandler / CommandHandler
                                                    ↓
                                        Repository(仓储) → DB / SLS
层职责依赖方向
adapter/接 HTTP 请求、参数校验、返回 VO→ Application
application/编排业务流程,通过 BusFacade 分发→ Domain + Infrastructure
domain/核心业务逻辑:聚合根 + 实体 + 事件不依赖任何层
infrastructure/持久化、外部 API、对象转换→ Domain(接口)

核心类

类作用
BusFacade唯一入口。sendQuery(query) / sendTransactCommand(cmd) / sendAsyncCommand(cmd)
BasePageQuery<PageVO<XxxVO>>所有分页 Query 的基类,带 pageNumber / pageSize(MAX_PAGE_SIZE=200)
QueryHandler<Q, R>实现 R handle(Q),读
CommandHandler<C, R>实现 R handle(C),写
EventHandler<T extends DomainEvent>实现 void handle(T),领域事件
AbstractAggregateRoot聚合根基类,addPendingEvent(event) 注册事件
WriteRepository<T>save/update/delete/operate 四个持久化方法
PageVO<T> / PageResult<T>Repository 返 PageVO,Controller 返 PageResult
Result<T>非分页接口的返回包装
BaseException统一异常(RuntimeException,throws 是文档性的)
ParamChecker参数校验,失败抛 BaseException(BAD_REQUEST)

文件命名

层文件说明
Domain 外层XxxData.javaSLS 数据包装(ts/source/endpoint/data)
Domain 内层XxxDetail.javaSLS 业务字段
Domain 聚合根{Name}.javaextends Entity
Domain 实体{Name}Entity.javaextends AbstractAggregateRoot
QueryListXxxQuery.java / GetXxxQuery.java读参数
CommandCreatXxxCommand.java写参数(注意拼写是 Creat 不是 Create,跟模板保持一致)
VOXxxVO.javaAPI 返回
DOXxxDo.javaMySQL 持久化
RepositoryXxxReadRepository.java / XxxWriteRepository.java接口在 domain,实现在 infrastructure
AssemblerXxxAssembler.java静态方法 toVo() / toDo() / toPageVo()
HandlerListXxxQueryHandler.java / CreatXxxCommandHandler.java实现对应泛型接口
ServiceXxxAppService.java + XxxAppServiceImpl.java接口 + @Service
ControllerXxxController.java@RestController @RequestMapping("/xxx")

两个硬约束

注意

这两条违反了编译能过、跑起来才炸,或者事件静默不发。每次写代码前先对照。

1. 泛型三处必须一致

text
ListXxxQuery extends BasePageQuery<PageVO<XxxVO>>
  ⇒ QueryHandler<ListXxxQuery, PageVO<XxxVO>>
  ⇒ XxxAppService.listXxx(...) 返回 PageVO<XxxVO>

三处的 R 必须是同一个类型。差一个泛型参数、把 XxxVO 写成 XxxData,都会出问题。

2. CommandHandler 必须返回聚合根

CommandHandler.handle(C) 的返回值是领域事件的载体。返回 void 或返回 VO,CqrsAroundHandler 就没东西可发,领域事件静默不发。

java
// ✅ 对
public class CreatXxxCommandHandler implements CommandHandler<CreatXxxCommand, Xxx> {
    public Xxx handle(CreatXxxCommand cmd) {
        Xxx agg = Xxx.create(cmd);          // 内部 addPendingEvent(...)
        writeRepository.save(agg);           // RepositoryEventAspect 发事件
        return agg;                          // ← 必须返回
    }
}

// ❌ 错:事件永远发不出去
public Void handle(CreatXxxCommand cmd) { writeRepository.save(...); return null; }

领域事件什么时候发

只有两条路径,缺一条就不发:

  1. CommandHandler 返回聚合根 / DomainEvents / DomainEvent —— CqrsAroundHandler 发。
  2. Repository 的 save / update / delete / operate 作用在 AbstractAggregateRoot 上 —— RepositoryEventAspect 在事务提交后发。

只改聚合根、返回 void、又不走 *Repository 持久化 → 事件不发。聚合根必须通过 *Repository 的方法落库。

配置项

yaml
soda:
  event:
    bus-type: simple          # simple | redis,默认 simple
    redis:                    # bus-type=redis 时才生效
      stream:
        group-name: my-group
        consumer-name: my-consumer
        idempotency:
          enabled: true

# 注意前缀是 scoda. 不是 soda.
scoda:
  ddd:
    cqrs:
      async:
        core-pool-size: 8
        max-pool-size: 16
注意

异步线程池的前缀是 scoda.ddd.cqrs.async.*,不是 soda.*。抄错前缀静默不生效。

构建与测试

bash
mvn clean verify          # 构建 + 测试(发版走这个)
mvn test                  # 只跑测试
mvn clean install         # 本地装库

Java 17 起,推荐 21。不要用 cp pom-3.2.xml pom.xml 那套多 POM 工作流 —— 已废弃,根目录的 pom-2.3.xml / pom-2.7.xml / pom-3.2.xml 是历史残留。

常见陷阱

警告

这些坑 AI 最容易踩,写代码前扫一眼。

  • Handler 按精确类匹配。SimpleCommandBus / SimpleQueryBus 用 handlers.get(command.getClass()) 查表,不走继承。给父类写的 Handler 不会接住子类命令。同一个命令类型只能有一个 CommandHandler,后注册的会覆盖前面的。
  • EventHandler 反而走继承。SimpleEventBus.publish 会沿父类和接口找,所以 EventHandler<DomainEvent> 能接住所有事件。
  • sendMqCommand 是桩,永远抛 UnsupportedOperationException。别调。
  • AbstractAggregateRoot extends AbstractDomainEvent —— 聚合根本身是个事件,身上有 eventId / requestId 这些字段。这是历史设计,业务上用不到,别去赋值。
  • @AutoConfigureAfter 用的是字符串类名,不是 Class 引用(为了兼容 Spring Boot 3 和 4)。加自动配置照这个写法。
  • PreChecker 已废弃,用 ParamChecker。
  • EventSerializer 是 Jackson 2/3 兼容的关键。要加 Jackson 相关注解,用 Jackson 2 的坐标 com.fasterxml.jackson.annotation.*,不要引 Jackson 3 的(Spring Boot 4 上会失效)。
  • knife4j-openapi3-spring-boot-starter 是 optional。子类化 BaseCommand / BaseQuery / AbstractDomainEvent 时,工程自己要带 io.swagger.core.v3:swagger-annotations,否则编译不过。

你不要做的事

  • 不要发明 @Command / @Query / @Handler 注解 —— soda 里没有任何自定义注解,全靠实现 CommandHandler / QueryHandler / EventHandler 接口 + @Component。
  • 不要在应用层直接注入 Repository 的实现类,只注入接口(否则应用层依赖了基础设施层)。
  • 不要绕过 BusFacade 直接调 Handler。
  • 不要在 Command 里写业务规则 —— 那是聚合根的职责。Command 只放业务参数,上下文字段由框架填充。