应用层落地模式
本文档介绍应用服务层的落地方式:WriteService 怎么搭、
execute()模板顺序、创建 / 修改 / 预校验三场景、规则装配与执行器选择。前置阅读:聚合目录落地骨架 · 聚合设计原则。
1. 本质与定位
应用服务层(Application)是编排领域逻辑的层:接收 Input → 建聚合(EntityFactory)/ 改聚合(EntityUpdater)→ 规则校验 → 仓储持久化 → 领域事件发布,全部由 execute() 模板统一编排,置于同一事务边界。
- 职责:编排(把 Factory / Updater / Rule / Repository / EventManager 串起来)。
- 不做:不写业务逻辑(业务逻辑在聚合根)、不操作持久化细节(仓储承担)、不做协议转换(UI 层承担)。
- 核心形态:应用服务分命令(写)与查询(读)两类——写服务
{Agg}WriteService继承AbstractApplicationService,每个用例一个公开方法、内部走execute()/tryExecute();读服务{Agg}ReadService无框架基类,注入领域层源端口、implements IQueryApplicationService,只读不写(见 §4.8)。
2. 命名与包结构
2.1 包结构
application/{agg}/
├── {Agg}WriteService.java # 命令应用服务(外层)
├── {Agg}ReadService.java # 查询应用服务(外层)
├── input/ # {Action}Input 业务语义入参
├── factory/ # EntityFactory 聚合工厂(创建场景)
├── updater/ # EntityUpdater 修改器(修改场景)
├── resolver/ # Command → 领域输入适配
├── rule/ # 规则装配(如 OrderRuleConfig)
├── service/ # 领域服务实现
└── subscriber/ # 事件订阅登记2.2 命名规范
| 类型 | 命名 | 示例 |
|---|---|---|
| 命令应用服务 | {Agg}WriteService | OrderWriteService |
| 查询应用服务 | {Agg}ReadService | OrderReadService |
| 入参 | {Action}Input | PayOrderInput |
| 工厂 | {Agg}Factory | OrderFactory |
| 修改器 | {Agg}{Action}Updater | OrderPayUpdater |
| 规则装配 | {Agg}RuleConfig | OrderRuleConfig |
3. 数据 / 职责承载
| 承载 | 不承载 |
|---|---|
| 编排依赖(Factory / Updater / Rule / Repository / EventManager) | 业务规则判断(聚合根 / 规则容器) |
| 事务边界与事件发布(委托执行器) | 持久化 SQL / 技术细节(仓储) |
| Input → 领域对象的组装与转换(Factory / Updater / Resolver) | 协议 Request / Response(UI 层) |
4. 落地方式(核心)
4.1 WriteService 骨架
@Service
public class OrderWriteService extends AbstractApplicationService
implements ICommandApplicationService {
private final OrderFactory orderFactory;
private final OrderRule orderRule;
private final OrderRepository orderRepository;
private final OrderPayUpdater orderPayUpdater;
// ... 其余 Updater
public OrderWriteService(IEventManager eventManager,
OutboxCommandExecutor commandExecutor,
Supplier<IUnitOfWork> unitOfWorkFactory,
OrderFactory orderFactory,
OrderRule orderRule,
OrderRepository orderRepository,
OrderPayUpdater orderPayUpdater) {
// 执行器与工厂由组合根(OutboxConfig)以成品 Bean 注入,服务内不再手动 new
super(eventManager, commandExecutor, unitOfWorkFactory);
this.orderFactory = orderFactory;
this.orderRule = orderRule;
this.orderRepository = orderRepository;
this.orderPayUpdater = orderPayUpdater;
}
/** 支付:加载聚合 → Updater 改聚合 → 统一校验 + 持久化 + 事件发布。 */
public Order payOrder(Long orderId, PayOrderInput input) {
Order order = orderRepository.findById(orderId);
if (order == null) {
return null;
}
return super.execute(order, orderRule, orderRepository, t -> orderPayUpdater.apply(t, input));
}
}要点:
- 继承
AbstractApplicationService,必须显式注入命令执行器与工作单元工厂(不再提供默认构造器)。 OutboxCommandExecutor与Supplier<IUnitOfWork>(outbox 工厂)由OutboxConfig以成品 Bean 提供,执行器与工厂语义须一致。- 每个用例一个公开方法:加载 / 创建聚合 →
execute(...)。 execute四参:聚合根、规则容器、仓储、领域逻辑(Consumer<T>)。
4.2 execute() 模板顺序
AbstractCommandExecutor.execute() 固定五步,子类只管"怎么落库 + 分发":
1. domainLogic.accept(aggregateRoot) 执行领域逻辑(Factory 建 / Updater 改)
2. satisfiesRule(rule) 规则校验,失败抛 BrokenRuleException
3. persistAndDispatch 落库(repository.save)+ 事件发布(eventManager.publish)
4. clearWorkUnitState() 事件 / 操作清空(防止跨请求串味)事件发布后的清理由模板内置,继承
AbstractApplicationService无需手动调用;仅在自研编排时需要。
4.3 创建场景:EntityFactory(先算后赋)
@Component
public class OrderFactory implements EntityFactory<Order, CreateOrderInput> {
@Override
public Order create(CreateOrderInput input) {
Long orderId = idGenerator.nextId();
List<OrderItem> items = totalAmountResolver.toOrderItems(input, orderId);
Customer customer = new Customer(input.getCustomerId(), input.getCustomerName());
Order probeOrder = new Order(probeData(customer), orderId); // 临时探测 Order
Money total = totalAmountResolver.resolve(input, probeOrder); // 先算派生属性
// ... 组装 OrderInitData 并 setTotalAmount(total),再 new Order(data, orderId) // 后赋
}
}EntityFactory<T, C> 契约:T create(C command),从 Command DTO 构建聚合,遵循「先算后赋」。WriteService 下单用例 orderFactory.create(input) 后交给 execute 落库。
4.4 修改场景:加载聚合 + EntityUpdater
@Component
public class OrderPayUpdater implements EntityUpdater<Order, PayOrderInput> {
@Override
public void apply(Order aggregateRoot, PayOrderInput command) {
PaymentInfo paymentInfo = new PaymentInfo(
command.getPaymentSerialNo(),
new Money(command.getPlatformDiscountAmount(), command.getCurrency()),
new Money(command.getAmount(), command.getCurrency()));
aggregateRoot.pay(paymentInfo); // 调聚合充血方法
}
}EntityUpdater<T, C> 契约:void apply(T aggregateRoot, C command)。职责 = Input → 领域对象转换 + 调充血方法;不做校验、不持久化、不发事件(由 execute 模板统一)。
4.5 预校验:tryExecute → DryRunResult
需要"先试跑不落库"的场景(如表单预校验 / 下单前检查):
public DryRunResult tryPayOrder(Long orderId, PayOrderInput input) {
Order order = orderRepository.findById(orderId);
if (order == null) {
return null;
}
return super.tryExecute(order, orderRule, orderRepository, t -> orderPayUpdater.apply(t, input));
}tryExecute 返回 DryRunResult(passed() / brokenRules()),不落库、不发事件,供预校验反馈。
4.6 规则装配:OrderRuleConfig
规则容器 OrderRule 的构造依赖(领域服务契约、仓储)由装配配置显式声明,领域层保持零 Spring 依赖:
@Configuration
public class OrderRuleConfig {
@Bean
public OrderRule orderRule(IOrderCustomerPermissionService permissionService,
IOrderRepository orderRepository) {
return new OrderRule(permissionService, orderRepository);
}
}规则容器的构造与触发完整落地见 聚合业务规则(OrderRule 范式)。
4.7 执行器选择
| 执行器 | 语义 | 场景 |
|---|---|---|
CommandExecutor | 落库后立即发布事件 | 不需要 Outbox 的事务一致性兜底 |
OutboxCommandExecutor | 聚合写 + outbox 行同事务,异步投递 | 跨模块可靠投递 / 崩溃兜底(见 Outbox 链路装配) |
AbstractApplicationService不再提供默认构造器,执行器与工作单元工厂必须由继承者显式注入(通常以组合根@Bean提供成品),且二者语义须一致:默认场景用CommandExecutor+UnitOfWork,outbox 场景用OutboxCommandExecutor+OutboxUnitOfWork,禁止混用。
4.8 读侧 ReadService:不进 execute() 模板
应用层有两类服务,写走命令({Agg}WriteService)、读走查询({Agg}ReadService),但读侧与写侧有本质差异:
| 维度 | WriteService(命令侧) | ReadService(查询侧) |
|---|---|---|
| 承载基类 | extends AbstractApplicationService(可写) | 无基类(只读,只 implements 标记接口) |
| 应用服务标记 | implements ICommandApplicationService | implements IQueryApplicationService |
是否走 execute() / tryExecute() | 走模板(校验 → 落库 → 发布事件) | 不走:只查,不建/改聚合 |
| 是否产生领域事件 / 写库 | 是(同步落库 + 事件发布) | 否:读不产生业务事件、不持有写仓储 |
| 依赖 | Factory / Updater / Rule / Repository / EventManager | 领域层源端口(IOrderRedisSource / IOrderESSource) |
读服务为什么不继承 AbstractApplicationService:读侧没有"改聚合 → 校验 → 落库 → 发事件"这一套写语义,不需要 execute() 模板。框架也不再提供 AbstractProjectionQuery 查询基类——「用哪个源」由「源端口 extends 了哪些查询族」在编译期决定,读服务只需注入领域源端口、按族分派并完成「查全量 → 裁剪」两跳:
@Service
public class OrderReadService implements IQueryApplicationService {
private final IOrderRedisSource redisSource;
private final IOrderESSource esSource;
public OrderReadService(IOrderRedisSource redisSource, IOrderESSource esSource) {
this.redisSource = redisSource;
this.esSource = esSource;
}
// 选源写死在方法体内(本项目:主键族走 Redis,条件族 / 分页族走 ES)
public <X extends IOrderProjection> X queryById(Long id, Class<X> projectionType) {
var full = redisSource.getById(id);
if (full == null) {
return null;
}
return reduceWith(redisSource.getReducer(projectionType), full, projectionType);
}
public <X extends IOrderProjection> List<X> queryList(OrderListQuery criteria, Class<X> projectionType) {
var reducer = esSource.getReducer(projectionType);
return esSource.search(criteria).stream()
.map(full -> reduceWith(reducer, full, projectionType))
.toList();
}
// queryByIds / queryOne / queryPage 同构;reduceWith 为私有两跳编排
}选源写在读服务方法体内,不外泄给调用方:不要把
ProjectionSource作为方法入参。读服务直接注入领域层源端口(框架已无ProjectorRegistry这一「源 id → 源实例」登记表,无从按 id 反查源),选源写死在方法体内。调用方只传条件与目标投影类型。
读服务的角色定位(为什么门面放应用层、选源写在这里)与两跳 / 裁剪的完整落地见 投影读模型代码落地指南。本小节只区分读写两侧的应用服务形态,不重复投影机制的细节。
⚠️ 读侧不发布领域事件、不持有写仓储:读模型由写侧事件物化而来(见 投影设计),
ReadService只消费读模型副本,不反向触发业务事件,避免读路径污染写一致性。
5. 关键机制与避坑
- 业务方法内"先
recordOperation后collectEvent":事件operationCode自动取最近一次操作;顺序颠倒抛OperationException。详见 操作注册表设计。 - 延迟事件(ID 后生成必用):构造期
entityId为null,用() -> XxxEvent.buildEvent(this),发布时才读真实 ID。详见 事件建模指南。 - 事务边界:
@Transactional由调用方(WriteService 方法 / 执行器)负责,仓储不管理事务。 - 异常响应映射:
BrokenRuleException(单条)/BrokenRuleAggregateException(全量)/PragmaticException(兜底),用@RestControllerAdvice统一映射:
@ExceptionHandler(BrokenRuleException.class)
public ResponseEntity<ErrorResponse> handleBrokenRule(BrokenRuleException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getCode(), e.getMessage()));
}6. 常见反模式
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| 应用服务里写业务规则 / 状态判断 | 业务逻辑泄漏到应用层、不可复用 | 业务逻辑内聚聚合根,校验走规则容器 |
绕过 execute() 手动编排放事件 / 清状态 | 模板顺序丢失、事件清理遗漏 | 统一走 execute() / tryExecute() |
| Updater / Factory 里做校验或持久化 | 职责混杂、模板被打断 | Updater 只转换 + 调充血方法;校验 / 持久化交模板 |
| 每个方法 new 一个执行器 / 事件管理器 | 资源浪费、语义漂移 | 构造器注入一次,复用 AbstractApplicationService |
| WriteService 直接操作仓储细节 / 批量 SQL | 仓储职责泄漏 | 复杂查询交查询侧,写仓储只收聚合根 |
7. 下一步
- 聚合目录落地骨架:应用层目录的落位
- 聚合业务规则(OrderRule 范式):规则容器的构造与触发
- 领域服务落地模式:应用层实现领域服务
- Outbox 链路装配:
OutboxCommandExecutor装配 - 核心:应用服务:
CommandExecutor/UnitOfWork/ Outbox 详解 - 投影读模型代码落地指南:
ReadService的完整落地(三跳取数 + Redis→ES 多源编排)