升级到 Spring Modulith 2.2.0-RC1 后,事件发布完成标记默认改到监听器事务里一起提交。监听器业务回滚时,完成标记也会一起回滚,不会出现「库存没落库,发布却已 COMPLETED」。要退回旧语义,配置 spring.modulith.events.completion=in-interceptor。

更早我们写过用测试守住 Modulith 模块边界。这次只盯持久化事件在监听器失败时,完成状态跟不跟业务同生共死。

场景:方法返回了,事务却要回滚

订单模块发布 OrderCompleted,库存用 @ApplicationModuleListener 写预留。故障不一定抛异常,也可能 setRollbackOnly():方法正常返回,事务标记回滚。若完成登记跑在拦截器里、又用独立事务提交,发布可能已是 COMPLETED,业务行却没有。

实测栈:Spring Boot 4.2.0-RC1、Spring Modulith 2.2.0-RC1、JPA 事件发布、H2。

<dependency>
  <groupId>org.springframework.modulith</groupId>
  <artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
@ApplicationModuleListener
void on(OrderCompleted event) {
  String mode = System.getProperty("demo.fail", "none");
  stocks.save(new StockEntry(event.orderId(), "reserved"));
  if ("throw".equals(mode)) {
    throw new IllegalStateException("inventory failed for " + event.orderId());
  }
  if ("rollback-only".equals(mode)) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return;
  }
}

2.2 的 spring.modulith.events.completion(CompletionTiming):

  • 默认 in-listener:监听器事务 beforeCommit 时标记完成,与业务同事务。
  • in-interceptor:监听器方法返回后,拦截器侧独立事务标记完成(旧行为)。

不要和 spring.modulith.events.completion-mode(UPDATE / DELETE / ARCHIVE)搞混。

默认:回滚后完成标记一起没了

成功路径库存有行,发布已完成:

2026-10-30T08:13:46.698+08:00  INFO ... InventoryListener : listener success orderId=order-ok
2026-10-30T08:13:46.699+08:00 DEBUG ... DefaultEventPublicationRegistry : Marking publication of event com.example.demo.order.OrderCompleted to listener com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) completed.
2026-10-30T08:13:48.993+08:00  INFO ... DemoRunner : stock count=1 presentForOrder=true
2026-10-30T08:13:49.001+08:00  INFO ... DemoRunner : pub-row ID=21e1adf0-aa06-4f34-8c8c-50a50a962a78 COMPLETION_ATTEMPTS=1 COMPLETION_DATE=2026-10-30T00:13:46.699912Z EVENT_TYPE=com.example.demo.order.OrderCompleted LAST_RESUBMISSION_DATE=2026-10-30T00:13:46.368392Z LISTENER_ID=com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) PUBLICATION_DATE=2026-10-30T00:13:46.368392Z SERIALIZED_EVENT={"orderId":"order-ok"} STATUS=COMPLETED

保存后抛异常:库存 0,STATUS=FAILED,无完成时间。

2026-10-30T08:19:23.301+08:00  INFO ... InventoryListener : listener throwing after save
2026-10-30T08:19:23.306+08:00 DEBUG ... CompletionRegisteringMethodInterceptor : Invocation of listener void com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) failed. Leaving event publication uncompleted.
java.lang.IllegalStateException: inventory failed for order-throw
2026-10-30T08:19:25.636+08:00  INFO ... DemoRunner : stock count=0 presentForOrder=false
2026-10-30T08:19:25.644+08:00  INFO ... DemoRunner : pub-row ID=9c6db27b-b65b-4f1d-b2ed-c61793266fc2 COMPLETION_ATTEMPTS=1 COMPLETION_DATE=null EVENT_TYPE=com.example.demo.order.OrderCompleted LAST_RESUBMISSION_DATE=2026-10-30T00:19:22.975997Z LISTENER_ID=com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) PUBLICATION_DATE=2026-10-30T00:19:22.975997Z SERIALIZED_EVENT={"orderId":"order-throw"} STATUS=FAILED

setRollbackOnly() 且方法正常返回:库存 0,发布停在 PROCESSING。

2026-10-30T08:19:48.493+08:00  INFO ... InventoryListener : listener setRollbackOnly after save
2026-10-30T08:19:50.809+08:00  INFO ... DemoRunner : stock count=0 presentForOrder=false
2026-10-30T08:19:50.817+08:00  INFO ... DemoRunner : pub-row ID=a25b0a50-735f-4fe3-8e41-94ab24429cc7 COMPLETION_ATTEMPTS=1 COMPLETION_DATE=null EVENT_TYPE=com.example.demo.order.OrderCompleted LAST_RESUBMISSION_DATE=2026-10-30T00:19:48.182383Z LISTENER_ID=com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) PUBLICATION_DATE=2026-10-30T00:19:48.182383Z SERIALIZED_EVENT={"orderId":"order-rb"} STATUS=PROCESSING

旧语义 in-interceptor:同样回滚,却已 COMPLETED

spring.modulith.events.completion=in-interceptor

再跑同一套 setRollbackOnly:库存仍是 0,但完成已单独提交。

2026-10-30T08:20:38.607+08:00  INFO ... InventoryListener : listener setRollbackOnly after save
2026-10-30T08:20:38.610+08:00 DEBUG ... DefaultEventPublicationRegistry : Marking publication of event com.example.demo.order.OrderCompleted to listener com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) completed.
2026-10-30T08:20:40.879+08:00  INFO ... DemoRunner : stock count=0 presentForOrder=false
2026-10-30T08:20:40.888+08:00  INFO ... DemoRunner : pub-row ID=c36b5615-4dfb-477b-ac98-26b67bbc8075 COMPLETION_ATTEMPTS=1 COMPLETION_DATE=2026-10-30T00:20:38.611148Z EVENT_TYPE=com.example.demo.order.OrderCompleted LAST_RESUBMISSION_DATE=2026-10-30T00:20:38.261202Z LISTENER_ID=com.example.demo.inventory.InventoryListener.on(com.example.demo.order.OrderCompleted) PUBLICATION_DATE=2026-10-30T00:20:38.261202Z SERIALIZED_EVENT={"orderId":"order-irb"} STATUS=COMPLETED

业务没落库,发布却标完成。重投和对账会当成「这个监听器已经处理过」。方法直接抛异常时,旧模式也会留下 FAILED;差别主要在「方法返回成功、事务却回滚」这条路径。

怎么选

能升到 Modulith 2.2 就保持默认 in-listener。只有依赖旧时序时再写 in-interceptor,并接受业务回滚但完成已提交的风险。对应变更见 #1961 与 2.2.0-RC1 发行说明。

— 感谢阅读 —
广告

一起交流

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