Skip to content

应用服务(Application Service)

本文档说明 io.pragmatic.ddd.application 包提供的应用服务编排能力:命令执行器、工作单元、应用服务基类、实体工厂/更新器/属性解析器,以及 Outbox 事务性发件箱。前置阅读:领域建模 · 业务规则引擎 · 领域事件

1. 概述

1.1 核心定位

应用服务层负责编排"领域逻辑 → 规则校验 → 持久化 → 事件分发"的标准流程,并对外部调用屏蔽模板细节。框架提供两类编排入口与一个便捷基类:

  • 单聚合根命令:一次操作只涉及一个聚合根,使用 CommandExecutor
  • 跨聚合根工作单元:一次操作涉及多个聚合根、需统一提交,使用 UnitOfWork
  • 应用服务基类AbstractApplicationService 内聚二者,提供语义化方法。

此外,Outbox 模式(application.outbox)提供事务性发件箱,保证事件与聚合根同事务原子落库。

两者均提供零副作用的试跑(Dry-run)入口,用于预览校验结论而不落库、不发事件。

1.2 概念层级与依赖关系

text
ICommandExecutor ──AbstractCommandExecutor── CommandExecutor(默认)
IUnitOfWork    ──AbstractUnitOfWork──── UnitOfWork(默认)/ OutboxUnitOfWork

AbstractApplicationService  聚合 ICommandExecutor + IUnitOfWork 工厂

EntityFactory<T,C>          创建新聚合根
EntityUpdater<T,C>          修改已有聚合根(apply)
IEntityPropertyResolver<C,E,R> / EntityPropertyResolvers  属性解析适配

application.outbox
 ├─ OutboxCommandExecutor / OutboxUnitOfWork   事务性发件箱执行器
 ├─ EagerOutboxPublisher                       提交后主动推送
 ├─ OutboxRelay + OutboxRelayConfig            兜底轮询补偿
 └─ OutboxStatus / OutboxMessage / OutboxEntry 状态机与载体
类型包路径用途
ICommandExecutor / AbstractCommandExecutor / CommandExecutorio.pragmatic.ddd.application单聚合根命令(默认:先 save 再逐条 publish)
IUnitOfWork / AbstractUnitOfWork / UnitOfWorkio.pragmatic.ddd.application跨聚合根工作单元(默认:统一 save 后 publishList)
AbstractApplicationServiceio.pragmatic.ddd.application应用服务便捷基类
DryRunResultio.pragmatic.ddd.application试跑(Dry-run)结构化结果(record)
EntityFactory / EntityUpdaterio.pragmatic.ddd.application实体创建 / 修改契约
IEntityPropertyResolver / EntityPropertyResolversio.pragmatic.ddd.application实体属性解析契约与适配器工厂
OutboxCommandExecutor / OutboxUnitOfWorkio.pragmatic.ddd.application.outbox事务性发件箱执行器
EagerOutboxPublisher / OutboxRelay / OutboxRelayConfigio.pragmatic.ddd.application.outbox主动推送 / 兜底轮询 / 运行配置
OutboxStatus / OutboxMessage / OutboxEntryio.pragmatic.ddd.application.outbox发件箱状态机与载体

2. 核心概念详解

2.1 单聚合根命令:ICommandExecutor / CommandExecutor

AbstractCommandExecutor 固定五步模板,CommandExecutor(默认)在 persistAndDispatch 钩子中先 save、再逐条 publish

text
1. 执行领域逻辑    domainLogic.accept(aggregateRoot)
2. 规则校验        aggregateRoot.satisfiesRule(rule) → 未通过则 throwBrokenRuleException
3. 持久化          repository.save(aggregateRoot)            ← persistAndDispatch 钩子
4. 发布事件        aggregateRoot.getDomainEvents().forEach(eventManager::publish)
5. 清空状态        aggregateRoot.clearWorkUnitState()
java
IEventManager eventManager = new ThreadPoolEventManager(LocalEventManagerConfig.defaultConfig());
eventManager.start();

CommandExecutor executor = new CommandExecutor(eventManager);
OrderRepository repository = new OrderRepository();
OrderRule rule = new OrderRule();

Order order = new Order(1L, "张三", 100);
Order result = executor.execute(order, rule, repository, Order::cancel);

试跑 Dry-run

tryExecute 执行与 execute 完全相同的领域逻辑与规则校验,但跳过持久化与事件分发,以 DryRunResult 结构化返回;rulenull 时视为无规则约束;仅规则类异常被转译为未通过,其余异常照常上抛。

java
DryRunResult result = executor.tryExecute(order, rule, repository, Order::cancel);
if (result.passed()) {
    // 校验通过
} else {
    List<BrokenRule> brokenRules = result.brokenRules();   // 规则违反明细
}

⚠️ 重要约束:试跑后的聚合根状态已变更(领域逻辑已执行),且 tryExecute 内部已清空暂存事件;该实例不可再用于真实 execute,应使用专用实例试跑。

2.2 跨聚合根工作单元:IUnitOfWork / UnitOfWork

AbstractUnitOfWork 固定多聚合根统一提交流程,划分为三阶段:领域逻辑与规则校验在事务外完成,持久化落在独立的数据库事务内,事件发布在事务提交之后

text
阶段一(事务外)validateAndCollect
  1. 逐条执行领域逻辑     domainLogic.accept
  2. 逐条规则校验         未通过则收集明细,全部违反聚合为 BrokenRuleAggregateException 抛出
  3. 汇总全部事件         collected.addAll(getDomainEvents)
     —— 任一违反即终止:事务根本不会开启,一个库都不碰 ——
阶段二(事务内)persistAndCollect
  4. 逐条持久化           repository.save(纯数据库写,事务由基类统一开启)
  5. 落 outbox / 清空     OutboxUnitOfWork 整批落 PENDING,随后 clearWorkUnitState
阶段三(事务外)dispatchEvents
  6. 统一发布             eventManager.publishList / EagerOutboxPublisher.publishAfterCommit

CommandExecutor 的区别:先全部校验,再统一落库,最后统一发布事件,适合需要事务一致性的多聚合根操作。事务是工作单元的固有语义:AbstractUnitOfWork 统一持有事务抽象,阶段二的所有 save(含同一聚合的多次操作)必然落在同一个数据库事务内,任一条目失败整体回滚。

java
try (IUnitOfWork uow = new UnitOfWork(eventManager, txOps)) {
    uow.register(order, orderRule, orderRepository, Order::cancel)
       .register(inventory, inventoryRule, inventoryRepository, Inventory::deduct)
       .commit();   // 事务外校验 → 事务内统一落库 → 事务外发布事件
}

⚠️ 重要约束:校验与持久化分离带来并发窗口。 阶段一校验通过到阶段二落库之间,聚合可能被其它请求修改。因此参与工作单元的聚合必须具备乐观锁版本doUpdate 须校验 affected rows,为 0 时抛乐观锁冲突异常使事务回滚。

⚠️ 事务是必填项: UnitOfWork 构造器必须显式注入事务抽象 TransactionOperations。无事务场景(如单测、纯本地事件)须显式传入 NoOpTransactionOperations,不能依赖默认。OutboxUnitOfWorkUnitOfWork 均共享基类同一事务边界。

UnitOfWork 实现 AutoCloseable:未提交时 close() 自动清空各条目事件,防止内存泄漏;commit()tryCommit() 均幂等保护,重复调用抛 IllegalStateException

阶段一或阶段二失败时(规则违反、落库异常),committed 保持 falseclose() 仍会清理暂存事件;仅阶段三发布失败时 committed 已为 true,不清理,由 OutboxRelay 兜底补偿。

试跑 tryCommit

tryCommit 复用阶段一的领域逻辑与规则校验,跳过持久化与事件分发;任一条目未通过时收集明细、不中断其余条目;试跑会消费工作单元,之后不可再 commit/tryCommit

java
DryRunResult result = unitOfWork.tryCommit();   // 零副作用,返回聚合全部条目校验结论

2.3 应用服务基类:AbstractApplicationService

应用服务基类,内聚 ICommandExecutorIUnitOfWork 工厂;继承者必须显式注入命令执行器与工作单元工厂,以明确声明自身的一致性语义。子类以语义方法暴露能力,无需直接持有编排器。

java
public class OrderApplicationService extends AbstractApplicationService {

    private final OrderRepository orderRepository;
    private final OrderRule orderRule;

    public OrderApplicationService(IEventManager eventManager,
                                   ICommandExecutor commandExecutor,
                                   Supplier<IUnitOfWork> unitOfWorkFactory,
                                   OrderRepository orderRepository,
                                   OrderRule orderRule) {
        super(eventManager, commandExecutor, unitOfWorkFactory);  // 显式声明执行器与工厂语义
        this.orderRepository = orderRepository;
        this.orderRule = orderRule;
    }

    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);
        execute(order, orderRule, orderRepository, Order::cancel);
    }

    public DryRunResult tryCancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);
        return tryExecute(order, orderRule, orderRepository, Order::cancel);
    }

    public void cancelWithInventory(Order order, Inventory inventory, ...) {
        try (IUnitOfWork uow = beginUnitOfWork()) {
            uow.register(order, orderRule, orderRepository, Order::cancel)
               .register(inventory, inventoryRule, inventoryRepository, Inventory::deduct)
               .commit();
        }
    }
}

唯一受保护构造器(必须显式注入执行器与工厂):

构造器用途
AbstractApplicationService(IEventManager, ICommandExecutor, Supplier<IUnitOfWork>)全自定义:注入命令执行器 + IUnitOfWork 工厂

执行器与工作单元必须语义一致:默认场景用 CommandExecutor + UnitOfWork,outbox 场景用 OutboxCommandExecutor + OutboxUnitOfWork,禁止混用,否则同一服务内出现两套一致性语义。装配通常交由组合根(如 @Configuration)以 Bean 提供成品。

重要约束AbstractApplicationService 仅为应用服务基类,不强制继承;也可直接组合 ICommandExecutor / IUnitOfWork 使用。

2.4 实体工厂、更新器与属性解析器

框架提供创建与修改聚合根的契约分离,以及属性解析的适配器工厂。

EntityFactory<T, C>:创建新实体

java
public interface EntityFactory<T extends AggregateRoot<?>, C> {
    T create(C command);   // 从 Command DTO 构建新聚合根,遵循"先算后赋"
}

EntityUpdater<T, C>:修改已有实体

java
public interface EntityUpdater<T extends AggregateRoot<?>, C> {
    void apply(T aggregateRoot, C command);   // 从 Command 计算变更并调用实体业务方法完成修改
}

IEntityPropertyResolver<C, E, R> / EntityPropertyResolvers:属性解析适配

IEntityPropertyResolver 在具体场景下从 Command DTO 与实体现状解析出实体属性值;EntityPropertyResolvers 适配器工厂将同一个实体属性计算领域服务(IEntityPropertyCalculator)适配到不同场景的 Command DTO,一处定义多处复用。

java
// 取数仅依赖 Command
IEntityPropertyResolver<CreateCmd, Order, BigDecimal> r1 =
        EntityPropertyResolvers.of(calculator, CreateCmd::getAmount);

// 取数需同时依赖 Command 与实体现状
IEntityPropertyResolver<UpdateCmd, Order, BigDecimal> r2 =
        EntityPropertyResolvers.of(calculator, (cmd, entity) -> cmd.extract(entity));

⚠️ 重要约束EntityUpdater 的方法是 apply(非 update);IEntityPropertyResolver 的方法是 resolve(C, E),创建场景下可调用 resolve(C)(entity 置 null)。

2.5 应用服务分层标记

java
public interface ICommandApplicationService { }   // 命令服务(写操作)
public interface IQueryApplicationService { }      // 查询服务(读操作)

读写分离标记,配合仓储的 IRepository(写)与 IAggregateProjection(读)实现读写分离:

java
public class OrderCommandService extends AbstractApplicationService
        implements ICommandApplicationService { /* 写操作 */ }

public class OrderQueryService implements IQueryApplicationService { /* 读操作 */ }

3. 关键机制与避坑指南

3.1 Outbox 事务性发件箱

OutboxCommandExecutor / OutboxUnitOfWork 与默认执行器并存、零侵入,保证事件与聚合根同事务原子落库:

text
① 同事务:聚合落库 + outbox 落库(PENDING)
② 事务提交后 → EagerOutboxPublisher.publishAfterCommit 主动推送(post-commit,规避"提交前误发")
③ 若主动推送失败/崩溃(未 markSent)→ OutboxRelay 兜底轮询补偿

EagerOutboxPublisher 在事务外发送原始事件,成功后 markSent(PENDING→SENT,带状态守卫、幂等);失败/崩溃不标记,保留 PENDING 交由 Relay 补偿。

java
OutboxCommandExecutor outboxExecutor = new OutboxCommandExecutor(
        outboxStore,            // IOutboxStore 实现(如 MybatisOutboxStore)
        transactionOperations,  // TransactionOperations(事务模板)
        eventSerializer,        // IEventSerializer
        eagerPublisher);        // EagerOutboxPublisher

outboxExecutor.execute(order, rule, repository, Order::cancel);   // 用法与 CommandExecutor 完全一致

3.2 Outbox 状态机与兜底轮询

OutboxStatus 四态:PENDING / PROCESSING / SENT / FAILED

text
PENDING ──claim──→ PROCESSING ──publish成功──→ SENT

                      └──publish失败──→ release → PENDING

                                   attempts > max?
                                      ├── 是 → FAILED(死信)
                                      └── 否 → 继续 PENDING

OutboxRelay 通过 claimPending(原子认领超时 PENDING 批次,WHERE status=PENDING AND created_at < now-grace LIMIT ?)补偿重发;OutboxRelayConfig.defaultConfig() 默认值:pollInterval=5min、grace=30s、batchSize=200、maxAttempts=10,亦可用 OutboxRelayConfig.bind(source)outbox 前缀从配置源绑定。

java
OutboxRelay relay = new OutboxRelay(
        outboxStore, eventManager, eventSerializer, scheduledExecutor,
        OutboxRelayConfig.defaultConfig());
relay.start();   // 启动周期性轮询(scheduleAtFixedRate)

⚠️ 重要约束:Outbox 执行器的事件推送发生在事务提交之后(post-commit);markSent 为独立短事务且带状态守卫,保证幂等。文档早期示例将 pollInterval/batchSize/maxAttempts 写为 5s/100/5 系旧值,正确默认值为 5min/200/10。

3.3 何时用 Outbox 替代默认执行器

场景推荐
单体应用,事件即时发布即可CommandExecutor / UnitOfWork
需保证事件与聚合根同事务原子性OutboxCommandExecutor / OutboxUnitOfWork
多实例部署,需防重复投递(claim_token 机制)Outbox(OutboxRelay.claimPending 原子认领)
事件投递可容忍少量延迟(兜底补偿)Outbox

4. 异常与错误处理体系

应用服务层主要复用框架既有异常,编排器自身不定义专属异常类型:

触发点异常说明
规则校验未通过RuleException(经 throwBrokenRuleExceptionCommandExecutor/UnitOfWork 提交时中断
试跑中规则失败转译为 DryRunResult.reject不抛异常ICommandExecutor.tryExecute / IUnitOfWork.tryCommit
工作单元重复提交/试跑IllegalStateExceptioncommit() / tryCommit() 幂等保护
Outbox 兜底失败超限OutboxRelay 标记 FAILED死信,不抛异常,需独立监控

重要约束:试跑(Dry-run)仅将规则类异常转译为未通过结论;领域逻辑中抛出的非规则异常(如空指针、基础设施异常)仍照常上抛,不会被吞掉。

5. 总结速查

概念使用方式最关键约束
单聚合根命令CommandExecutor.execute(t, rule, repo, logic)默认先 save 再逐条 publish
跨聚合根工作单元UnitOfWork.register(...).commit()(try-with-resources)统一校验→落库→发布;close() 未提交自动清理
试跑tryExecute / tryCommitDryRunResult零副作用;试跑实例不可复用;仅规则异常转译
应用服务基类继承 AbstractApplicationService,显式注入 ICommandExecutor + Supplier<IUnitOfWork>提供 execute/tryExecute/beginUnitOfWork;非强制
实体创建/修改EntityFactory.create / EntityUpdater.applyapplyupdate
属性解析EntityPropertyResolvers.of(calc, extractor)一处定义多处复用
分层标记ICommandApplicationService / IQueryApplicationService配合 IRepository/IAggregateProjection 读写分离
Outbox 执行器OutboxCommandExecutor / OutboxUnitOfWork同事务落库;post-commit 推送
Outbox 兜底OutboxRelay.start() + OutboxRelayConfig默认 5min/30s/200/10;四态状态机

6. 命名规范速查

结合框架事实约束(接口以 I 开头、执行器/工作单元语义命名、Outbox 类以 Outbox 前缀、方法名定论),约定如下:

元素格式示例
命令执行器接口/类ICommandExecutor / {语义}CommandExecutorICommandExecutorCommandExecutor
工作单元接口/类IUnitOfWork / {语义}UnitOfWorkIUnitOfWorkUnitOfWorkOutboxUnitOfWork
应用服务基类Abstract{层}ServiceAbstractApplicationService
命令服务类{聚合}CommandService implements ICommandApplicationServiceOrderCommandService
查询服务类{聚合}QueryService implements IQueryApplicationServiceOrderQueryService
语义方法execute / tryExecute / beginUnitOfWork / commit / tryCommit基类已定义,子类按需派生 cancelOrder
实体工厂{聚合}Factory implements EntityFactory<T,C>OrderFactory
实体更新器{聚合}Updater implements EntityUpdater<T,C>(方法 applyOrderUpdater
属性解析器{语义}Resolver implements IEntityPropertyResolver<C,E,R>DiscountResolver
Outbox 类Outbox{角色}OutboxCommandExecutorOutboxRelayOutboxRelayConfigEagerOutboxPublisher
试跑结果DryRunResultpass() / reject(...)DryRunResult

⚠️ 重要约束EntityUpdater 的方法名为 apply(非 update);IEntityPropertyResolver 的创建场景便捷方法为 resolve(C)。命名误用将导致编译失败。Outbox 配置键统一以 outbox 为前缀(outbox.poll-interval / outbox.grace / outbox.batch-size / outbox.max-attempts),由 OutboxRelayConfig.bind 绑定。

下一步阅读