EventSourcingDB 在 2026 年 10 月 9 日发布了官方 Java Client SDK 1.0,Spring Boot 4 项目引入 io.thenativeweb:eventsourcingdb-spring-boot-starter:1.0.0、配好 base-url 和 api-token 两个属性,就能注入 Client 写入和读取事件。集成测试用它自带的 Testcontainers 模块加 @ServiceConnection,不用写连接配置就能连上容器里的真实数据库。SDK 本身是 MIT 开源,但它连的 EventSourcingDB 是闭源商用数据库,有免费档,超出后按实例收费。我们在 JDK 21、Spring Boot 4.1.1 上建了示例项目,写入、读取和容器测试都跑通了。
下面的代码都出自这个示例项目,API、类名和配置键对照的是 GitHub 仓库 v1.0.0 标签下的 README、源码和 Maven Central 上的 POM。
三个构件和版本
Maven Central 上 io.thenativeweb 组下有三个构件,版本都是 1.0.0:
| artifactId | 用途 | 主要传递依赖(取自发布的 POM) |
|---|---|---|
eventsourcingdb |
客户端本体 | tools.jackson.core:jackson-databind:3.1.5、org.jspecify:jspecify:1.0.1 |
eventsourcingdb-testcontainers |
测试容器 Container |
org.testcontainers:testcontainers:2.0.5 |
eventsourcingdb-spring-boot-starter |
自动配置 Client Bean |
org.springframework.boot:spring-boot-starter:4.1.1 |
普通 Java 项目只需要第一个,直接 new Client(URI.create("http://localhost:3000"), "secret") 即可。Spring Boot 项目用 starter,它会带上客户端本体。示例项目的父 POM 是 spring-boot-starter-parent:4.1.1,依赖部分如下:
<properties>
<java.version>21</java.version>
<eventsourcingdb.version>1.0.0</eventsourcingdb.version>
</properties>
<dependencies>
<dependency>
<groupId>io.thenativeweb</groupId>
<artifactId>eventsourcingdb-spring-boot-starter</artifactId>
<version>${eventsourcingdb.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.thenativeweb</groupId>
<artifactId>eventsourcingdb-testcontainers</artifactId>
<version>${eventsourcingdb.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
spring-boot-starter-jackson 是为了让容器里有 Spring Boot 的 JsonMapper,starter 检测到它就拿来序列化事件数据,没有的话客户端会用自带的 mapper。testcontainers-junit-jupiter 是 Testcontainers 2.x 的新 artifactId,版本由 Boot 管理。
配置和最小的写入、读取
starter 的属性前缀是 eventsourcingdb,只有两个键,对应源码里的 EventSourcingDbProperties:
eventsourcingdb.base-url=http://localhost:3000
eventsourcingdb.api-token=secret
自动配置类 EventSourcingDbAutoConfiguration 据此创建 Client Bean。下面的 BookService 写一个事件再按 subject 读回来:
public record BookAcquired(String title, String author, String isbn) {}
@Service
public class BookService {
private static final String SOURCE = "https://library.eventsourcingdb.io";
private static final String BOOK_ACQUIRED = "io.eventsourcingdb.library.book-acquired";
private final Client client;
public BookService(Client client) {
this.client = client;
}
public Event acquire(String bookId, BookAcquired book) {
var candidate = new EventCandidate(SOURCE, "/books/" + bookId, BOOK_ACQUIRED, book);
return client.writeEvents(List.of(candidate)).getFirst();
}
public List<BookAcquired> history(String bookId) {
try (var events = client.readEvents("/books/" + bookId, new ReadEventsOptions(false))) {
return events.map(event -> event.data(BookAcquired.class)).toList();
}
}
}
EventCandidate 是还没被数据库接受的事件,四个参数依次是 source、subject、type、data,格式遵循 CloudEvents。source 是标识来源系统的 URI,不会被真的访问。writeEvents 返回 List<Event>,里面带着服务端补上的 id、时间和哈希。
readEvents 返回惰性的 Stream<Event>。它占着一条 HTTP 连接,所以必须用 try-with-resources 关掉。ReadEventsOptions(false) 只读 /books/42 本身,传 true 会连子 subject 一起读。
写入时还可以带前置条件做并发控制。1.0 提供 IsSubjectPristinePrecondition、IsSubjectPopulatedPrecondition、IsSubjectOnEventIdPrecondition、IsEventQlQueryTruePrecondition 四种,作为 writeEvents 的第二个参数传入,条件不满足时抛 DbApiException(409)。另外注意 ping() 不校验 token,要验证 token 得用 verifyApiToken()。
Client 是 AutoCloseable,应用关闭时由 Spring 关闭。starter 的 Bean 都带 @ConditionalOnMissingBean,需要自定义 HTTP 客户端时可以自己声明 Client Bean,再注入 EventSourcingDbConnectionDetails 取地址和 token,写法见 README。
用 @ServiceConnection 写集成测试
测试容器类是 io.thenativeweb.eventsourcingdb.testcontainers.Container,继承自 Testcontainers 的 GenericContainer。starter 通过 spring.factories 注册了 ConnectionDetailsFactory,测试 classpath 上同时有它和 spring-boot-testcontainers 时,连接信息从容器取。
示例项目用的是 @Testcontainers 加静态字段的写法。SDK 的容器类也叫 Container,和 Testcontainers 的 @Container 注解重名,Java 不能同时 import 两个同名类型,所以注解写全限定名:
@SpringBootTest
@Testcontainers
class BookServiceIntegrationTest {
@org.testcontainers.junit.jupiter.Container
@ServiceConnection
static Container eventSourcingDb = new Container().withImageTag("1.2.0");
@Autowired
BookService books;
@Autowired
Client client;
@Test
void writesAndReadsBackAnEvent() {
client.ping();
client.verifyApiToken();
var written = books.acquire("42",
new BookAcquired("2001 - A Space Odyssey", "Arthur C. Clarke", "978-0756906788"));
assertThat(written.subject()).isEqualTo("/books/42");
assertThat(written.type()).isEqualTo("io.eventsourcingdb.library.book-acquired");
assertThat(books.history("42"))
.containsExactly(new BookAcquired("2001 - A Space Odyssey", "Arthur C. Clarke", "978-0756906788"));
}
}
import 部分:Container 来自 io.thenativeweb.eventsourcingdb.testcontainers,@ServiceConnection 来自 org.springframework.boot.testcontainers.service.connection,@Testcontainers 来自 org.testcontainers.junit.jupiter。README 里另一种写法是 @TestConfiguration 里把 Container 声明为 @Bean @ServiceConnection,再 @Import 到测试类。
Container 默认用镜像 thenativeweb/eventsourcingdb:latest,测试里用 withImageTag("1.2.0") 固定了版本,CI 结果更容易复现。
实际跑一遍
环境:Debian 13 上的 OpenJDK 21.0.12.1、Maven 3.9.9、Docker 26.1.5(apt 安装),Spring Boot 4.1.1,Testcontainers 2.0.5,镜像 thenativeweb/eventsourcingdb:1.2.0(digest sha256:905b4b06831e…,跑的时候与 latest 指向同一个 digest)。
mvn test 的关键输出:
INFO org.testcontainers.DockerClientFactory -- Testcontainers version: 2.0.5
INFO tc.thenativeweb/eventsourcingdb:1.2.0 -- Container thenativeweb/eventsourcingdb:1.2.0 started in PT0.342799334S
INFO c.e.esdbdemo.BookServiceIntegrationTest : Started BookServiceIntegrationTest in 0.635 seconds (process running for 3.445)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.829 s -- in com.example.esdbdemo.BookServiceIntegrationTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.example.esdbdemo.BookAcquiredJsonTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
第二个测试 BookAcquiredJsonTest 不依赖 Docker,只验证事件记录能用 Jackson 3 的 JsonMapper 来回序列化。
跑测试时 application.properties 里还写着 http://localhost:3000,本机 3000 端口上也没有服务,测试照样通过,可见连接信息已经被 @ServiceConnection 换成了容器地址。
除了测试,我们还用 Docker 单独起了一个实例,让 Boot 应用按配置文件连接:
docker run -d --name esdb-demo -p 3000:3000 thenativeweb/eventsourcingdb:1.2.0 \
run --api-token secret --data-directory-temporary --http-enabled --https-enabled=false
这组启动参数与 SDK 的 Container 源码里使用的一致。应用启动后写一条、读一条,输出:
:: Spring Boot :: (v4.1.1)
Started EsdbDemoApplication in 0.707 seconds (process running for 0.906)
written id=0 subject=/books/42 type=io.eventsourcingdb.library.book-acquired
read back: [BookAcquired[title=2001 - A Space Odyssey, author=Arthur C. Clarke, isbn=978-0756906788]]
实例启动日志里有一行 "evaluation license has limited capacity","maxEvents":25000,与官方文档的免费档上限对得上。
跑的过程中碰到的几个问题:
- 两个
Container重名:见上文,注解写全限定名。 - Testcontainers 2.x 的 JUnit 模块改名:要用
org.testcontainers:testcontainers-junit-jupiter。 - 属性缺失时的报错:把
eventsourcingdb.api-token设为空,应用启动失败,根因是java.lang.IllegalStateException: eventsourcingdb.api-token must be set,和 README 说的一致。 - token 写错:starter 启动时不校验 token,应用照常启动,第一次写入时才失败:
DbApiException: failed to write events, got HTTP status code '401', expected '200': unauthorized。README 也提醒ping()不校验 token,健康检查要用verifyApiToken()。 - Jackson 版本:Spring Boot 4.1.1 管理的 Jackson 正好是 3.1.5,与 SDK POM 一致,
mvn dependency:tree里只有一份tools.jackson.core:jackson-databind:3.1.5,没有冲突(jackson-annotations仍是 2.x 的 2.21,这是 Jackson 3 自身的安排)。项目里如果固定了其他 3.x 版本,升级前自己跑一下测试。 - 容器启动等待:SDK 的
Container等待/api/v1/ping返回,超时 10 秒;本次容器 0.34 秒就绪,没有碰到超时。 - 启动日志里的授权类型:本次用 1.2.0 镜像、不带 license key 启动,日志显示 license 类型为 evaluation,
expiresAt约为启动后两天。原始日志行是{"time":"2026-10-09T05:32:07.921269629Z","level":"INFO","msg":"using license","type":"evaluation","licensedTo":"n/a","expiresAt":"2026-10-11T05:32:07.921259268Z"},其中2026-10-11T05:32:07Z即北京时间 10 月 11 日 13:32。官方文档对免费档的说法是 25,000 事件以内免费、不需要 license key。 - Docker 环境本身:这台机器上 dockerd 需要手动以 root 启动,overlay2 挂载失败(
failed to mount overlay: invalid argument),自动退回 vfs 存储驱动,功能正常只是占空间。当前用户还要加入docker组,Testcontainers 才能访问/var/run/docker.sock。这些都是这台机器自己的问题,和 SDK 无关。
Java 21、Jackson 3 和 JSpecify 的实际影响
SDK 用 Java 21 工具链构建,README 写明要 Java 21 及以上,低于这个版本没法用。示例项目里的 List.getFirst() 也是 Java 21 才有的方法。
SDK 依赖的是 Jackson 3 的 tools.jackson.core:jackson-databind,事件的无参 data() 返回 Jackson 3 的 JsonNode。所以 starter 要求 Spring Boot 4,还在 Spring Boot 3.x 加 Jackson 2 的项目用不上它。
JSpecify 的影响主要在静态检查上。API 用它标注了空值语义,IDE 和 NullAway 能据此判断哪里可以传 null。使用 JPMS 的项目 requires io.thenativeweb.eventsourcingdb; 即可,该模块已经 requires transitive 了 tools.jackson.databind、org.jspecify 和 java.net.http。
客户端抛出的异常都是非受检异常,自有异常继承 EventSourcingDbException,不需要在业务代码里层层声明 throws。
授权与收费边界
SDK 的三个构件都是 MIT 许可(仓库 LICENSE.md 与 POM 一致),版权方是 the native web GmbH。
数据库本身是另一回事。官方 Licensing 文档写明 EventSourcingDB 是闭源、商业维护的产品,存储事件数在 25,000 以内可以免费用,开发和生产都不需要 license key。超出后按实例收费,每个实例单独一份授权,具体条款和价格见官方 Licensing 页面。
一起交流
分享你的思考,让讨论更进一步。