用 Java 编写领域
Wow 编译器读取 Java 源码就像读取 Kotlin 源码一样:同样的注解、同样的生成模型、同样的 Manifest。领域模块可以用 Java 写、用 Kotlin 写,或者两者混用。本页用 Java 写一个小的账户聚合,并列出 Java 目前还做不到的事。它就是仓库里的 Java 冒烟测试 examples/java-smoke,每次改动构建都会编译并测试它。
准备模块
Java 领域模块和 Kotlin 领域模块一样,应用 Kotlin JVM 插件 kotlin("jvm") 和 Gradle 插件 me.ahoo.wow:KSP 在 Kotlin 编译中处理模块的 Java 源码,它生成的模型是 Kotlin 代码。没有 Kotlin JVM 插件的模块,插件会配置失败,并指出修正方法。冒烟测试作为 Wow 自己构建中的一个项目,也是这样应用的。
命令与事件
命令和事件是 Java record。@Summary 在 Manifest 中为它们写说明:
package me.ahoo.wow.example.java;
import me.ahoo.wow.api.annotation.Summary;
/** Withdraw money. */
@Summary("Withdraw money")
public record Withdraw(long amount) {
}聚合
根是一个标注了 @AggregateRoot 的类,带一个接收其状态的构造函数。它的命令函数是普通的 public 方法,按注解或按名字 onCommand 找到:
package me.ahoo.wow.example.java;
import me.ahoo.wow.api.annotation.AggregateRoot;
import me.ahoo.wow.api.decide.RejectedException;
import me.ahoo.wow.api.decide.Rejection;
/**
* An aggregate root written in Java. Its decide functions are plain Java methods: they return the event they
* produce, and reject a command by throwing a {@link RejectedException} of a {@link Rejection}. A Java decide function cannot call the {@code suspend} functions of the API,
* such as {@code DecideScope.reserve}, and the test DSL is Kotlin, so the tests of a Java domain are Kotlin.
*/
@AggregateRoot
public class Account {
private final AccountState state;
public Account(AccountState state) {
this.state = state;
}
public AccountOpened onCommand(OpenAccount command) {
return new AccountOpened(command.owner());
}
public Deposited onCommand(Deposit command) {
requirePositive(command.amount());
return new Deposited(command.amount());
}
public Withdrawn onCommand(Withdraw command) {
requirePositive(command.amount());
if (command.amount() > state.getBalance()) {
throw new RejectedException(
Rejection.of(AccountRejections.INSUFFICIENT_BALANCE, "The balance is " + state.getBalance() + ".")
);
}
return new Withdrawn(command.amount());
}
private static void requirePositive(long amount) {
if (amount <= 0) {
throw new RejectedException(Rejection.of(AccountRejections.INVALID_AMOUNT, amount + " is not positive."));
}
}
}- 方法返回它的事件,或返回
null表示不做改变,通过抛出new RejectedException(Rejection.of(code, message))拒绝。返回类型无法指明事件时,返回一个用静态方法Decision.events(...)和Decision.reject(code, message)构建的Decision;Decision.NoOp.INSTANCE什么都不改变。返回类型是接口或抽象类、又不是密封的,会导致编译错误,因为它没有指明事件。 - 返回
Decision的方法要在@OnCommand(produces = ...)中指明它的事件,与 Kotlin 一样。 - 方法可以在命令之后接收领域服务和
DecideScope,也可以接收Command<X>而不是X。
状态
状态是一个可变类,每个事件对应一个 public 的 onSourcing 方法。它的 getter 就是 Jackson 序列化进快照的内容,也是测试 DSL 用来比较、以检查决定没有修改状态的内容:
package me.ahoo.wow.example.java;
/**
* The account's state: mutable, evolved by its public {@code onSourcing} methods, one per event. Its getters are what
* Jackson serializes (snapshots, the test DSL's purity check).
*/
public class AccountState {
private String owner = "";
private long balance;
public String getOwner() {
return owner;
}
public long getBalance() {
return balance;
}
public void onSourcing(AccountOpened event) {
owner = event.owner();
}
public void onSourcing(Deposited event) {
balance += event.amount();
}
public void onSourcing(Withdrawn event) {
balance -= event.amount();
}
}拒绝错误码
@ErrorCode 写在一个 public static final String 字段上。不带点的错误码是本地名,声明在根类或嵌套在根类中的类的字段上;带点的错误码是完整的,就像这些:
package me.ahoo.wow.example.java;
import me.ahoo.wow.api.annotation.ErrorCode;
/** The account's rejection codes: {@code @ErrorCode} on Java {@code static final String} fields. */
public final class AccountRejections {
@ErrorCode(description = "An amount is always positive.")
public static final String INVALID_AMOUNT = "smoke.account.invalid-amount";
@ErrorCode(description = "The balance does not cover the withdrawal.", status = 409)
public static final String INSUFFICIENT_BALANCE = "smoke.account.insufficient-balance";
private AccountRejections() {
}
}测试
测试 DSL 是 Kotlin 写的,所以 Java 领域的测试用 Kotlin 写,放在同一个模块里。它们运行编译器从 Java 源码生成的模型:
@Test
fun `a Java rejection should carry the code of a Java field`() = aggregateSpec(model, id = "alice") {
given(AccountOpened("Alice"), Deposited(100))
whenCommand(Withdraw(150)) { expectRejected(AccountRejections.INSUFFICIENT_BALANCE) }
whenCommand(Deposit(0)) { expectRejected(AccountRejections.INVALID_AMOUNT) }
whenCommand(Withdraw(30)) {
expectEvents(Withdrawn(30))
expectState { balance.assert().isEqualTo(70) }
}
}Java 目前还做不到的事
API 中有一部分是 Kotlin 的 suspend 函数,Java 无法调用:
| 只在 Kotlin 中 | 在 Java 中改为 |
|---|---|
DecideScope.reserve | 用 Kotlin 编写预留唯一键的聚合 |
SagaContext.call,对另一个上下文的同步调用 | 派生命令:返回它,由命令主题投递 |
| 测试 DSL | 用 Kotlin 编写测试 |
Java 写的应用代码通过命令客户端和查询客户端的 Java 门面 JavaCommandClient 和 JavaQueryApi 调用它们,调用是阻塞的,或者返回 CompletableFuture(调用其他服务)。