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 页面。

参考链接

— 感谢阅读 —

一起交流

分享你的思考,让讨论更进一步。