关于 clean-architecture
这项技能为 Java 21+、Spring Boot 3.5+ 应用提供整洁架构、六边形架构和 DDD 战术模式指导,强调框架无关的领域逻辑、依赖内向和分层测试。
主要功能
- 01四层结构与依赖规则指导
- 02框架无关的领域建模
- 03端口与适配器设计
- 04DDD 实体、值对象和事件
- 05分层测试与架构合规检查
适用场景
- 01规划新的 Spring Boot 应用结构
- 02重构紧耦合代码
- 03将领域逻辑与框架分离
- 04在无 Spring 上下文时测试业务逻辑
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill clean-architecture安装后在 Claude Code 中输入/clean-architecture确认已加载。
附带文件references/java-clean-architecture.mdreferences/spring-boot-implementation.md
详细介绍
Spring Boot 的整洁架构、六边形架构与 DDD
概述
本技能全面介绍如何在 Java 21+ Spring Boot 3.5+ 应用中实现整洁架构、六边形架构(端口与适配器)以及领域驱动设计的战术模式。通过恰当的分层和依赖管理,确保职责清晰分离、领域逻辑独立于框架,并打造高度可测试的代码库。
适用场景
- 为职责清晰分离的新 Spring Boot 应用设计架构
- 将紧密耦合的代码重构为可测试的分层架构
- 实现独立于框架和基础设施的领域逻辑
- 设计可替换实现的端口和适配器
- 应用领域驱动设计的战术模式(实体、值对象、聚合)
- 创建不依赖 Spring 上下文的可测试业务逻辑
说明
1. 理解核心概念
整洁架构分层(依赖规则)
依赖向内流动。内层不应了解外层。
| 层 | 职责 | Spring Boot 对应项 |
|---|---|---|
| 领域 | 实体、值对象、领域事件、仓储接口 | domain/ - 无 Spring 注解 |
| 应用 | 用例、应用服务、DTO、端口 | application/ - @Service、@Transactional |
| 基础设施 | 框架、数据库、外部 API | infrastructure/ - @Repository、@Entity |
| 适配器 | 控制器、表示器、外部网关 | adapter/ - @RestController |
六边形架构(端口与适配器)
- 领域核心:纯 Java 业务逻辑,不依赖框架
- 端口:定义契约的接口(驱动端和被驱动端)
- 适配器:具体实现(JPA、REST、消息传递)
领域驱动设计战术模式
- 实体:具有身份标识和生命周期的对象(例如
Order、Customer) - 值对象:不可变,由属性定义(例如
Money、Email) - 聚合:以根实体为界限的一致性边界
- 领域事件:记录重要的业务事件
- 仓储:持久化抽象,在基础设施层中实现
2. 组织包结构
遵循以下基于功能的包组织方式:
com.example.order/
├── domain/
│ ├── model/ # Entities, value objects
│ ├── event/ # Domain events
│ ├── repository/ # Repository interfaces (ports)
│ └── exception/ # Domain exceptions
├── application/
│ ├── port/in/ # Driving ports (use case interfaces)
│ ├── port/out/ # Driven ports (external service interfaces)
│ ├── service/ # Application services
│ └── dto/ # Request/response DTOs
├── infrastructure/
│ ├── persistence/ # JPA entities, repository adapters
│ └── external/ # External service adapters
└── adapter/
└── rest/ # REST controllers
3. 实现领域层(不依赖框架)
领域层不得依赖 Spring 或任何框架。
- 使用 Java record 创建带内置验证的不可变值对象
- 将业务逻辑放在实体中,而非服务中(充血领域模型)
- 在领域层定义仓储接口(端口)
- 使用强类型 ID,避免 ID 混淆
- 实现领域事件,以解耦副作用
- 使用工厂方法创建实体,以确保不变量
4. 实现应用层
- 在
application/port/in/中创建用例接口(驱动端口) - 在
application/port/out/中创建外部服务接口(被驱动端口) - 使用
@Service和@Transactional实现应用服务 - 使用 DTO 处理请求和响应,并与领域模型分开
- 操作成功后发布领域事件
5. 实现基础设施层(适配器)
- 在
infrastructure/persistence/中创建 JPA 实体 - 实现仓储适配器,在领域实体与 JPA 实体之间进行映射
- 使用 MapStruct 或手动映射器进行领域对象与 JPA 对象之间的转换
- 配置条件 Bean,以支持替换实现
- 将基础设施相关事项与领域逻辑隔离
6. 实现适配器层(REST)
- 在
adapter/rest/中创建 REST 控制器 - 注入用例接口,而非实现类
- 对 DTO 使用 Bean Validation
- 返回恰当的 HTTP 状态码和响应
- 使用全局异常处理器处理异常
7. 应用最佳实践
- 依赖规则:领域层不依赖 Spring 或其他框架
- 不可变值对象:使用 Java record 创建带内置校验的值对象
- 富领域模型:将业务逻辑放在实体中,而不是服务中
- 仓储模式:由领域层定义接口,由基础设施层实现
- 领域事件:将副作用与主要操作解耦
- 构造器注入:通过 final 字段注入必需依赖
- DTO 映射:将领域模型与 API 契约分开
- 事务边界:在应用服务中放置
@Transactional - 工厂方法:使用
Entity.create()在构造时确保不变量 - 分离 JPA 实体:通过映射器将领域实体与 JPA 实体分开
8. 验证架构合规性
实现每一层后,验证依赖规则是否得到遵守:
- 领域层检查:运行
grep -r "@Service\|@Component\|@Autowired" domain/,确保没有 Spring 导入 - ArchUnit 测试:添加依赖测试,验证领域层没有导入基础设施:
noClasses().that().resideInPackage("..domain..") .should().accessClassesThat().resideInAnyPackage("..spring..", "..infrastructure.."); - 实体暴露检查:验证领域服务绝不返回 JPA 实体
- 事务检查:确认
@Transactional 只出现在应用层服务中,绝不出现在领域层
9. 编写测试
- 领域测试:不加载 Spring 上下文的纯单元测试,执行速度快
- 应用测试:使用 Mockito 模拟端口的单元测试
- 基础设施测试:使用
@DataJpaTest 和 Testcontainers 进行集成测试 - 适配器测试:使用
@WebMvcTest 编写控制器测试
示例
示例 1:领域层 - 带领域事件的实体
// domain/model/Order.java
public class Order {
private final OrderId id;
private final List<OrderItem> items;
private Money total;
private OrderStatus status;
private final List<DomainEvent> domainEvents = new ArrayList<>();
private Order(OrderId id, List<OrderItem> items) {
this.id = id;
this.items = new ArrayList<>(items);
this.status = OrderStatus.PENDING;
calculateTotal();
}
public static Order create(List<OrderItem> items) {
validateItems(items);
Order order = new Order(OrderId.generate(), items);
order.domainEvents.add(new OrderCreatedEvent(order.id, order.total));
return order;
}
public void confirm() {
if (status != OrderStatus.PENDING) {
throw new DomainException("Only pending orders can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
}
public List<DomainEvent> getDomainEvents() {
return List.copyOf(domainEvents);
}
public void clearDomainEvents() {
domainEvents.clear();
}
}
示例 2:领域层 - 带校验的值对象
// domain/model/Money.java (Value Object)
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new DomainException("Amount cannot be negative");
}
}
public static Money zero() {
return new Money(BigDecimal.ZERO, Currency.getInstance("EUR"));
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new DomainException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
示例 3:领域层 - 仓储端口
// domain/repository/OrderRepository.java (Port)
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
}
示例 4:应用层 - 用例和服务
// application/port/in/CreateOrderUseCase.java
public interface CreateOrderUseCase {
OrderResponse createOrder(CreateOrderRequest request);
}
// application/dto/CreateOrderRequest.java
public record CreateOrderRequest(
@NotNull UUID customerId,
@NotEmpty List<OrderItemRequest> items
) {}
// application/service/OrderService.java
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final DomainEventPublisher eventPublisher;
@Override
public OrderResponse createOrder(CreateOrderRequest request) {
List<OrderItem> items = mapItems(request.items());
Order order = Order.create(items);
PaymentResult payment = paymentGateway.charge(order.getTotal());
if (!payment.successful()) {
throw new PaymentFailedException("Payment failed");
}
order.confirm();
Order saved = orderRepository.save(order);
publishEvents(order);
return OrderMapper.toResponse(saved);
}
private void publishEvents(Order order) {
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}
示例 5:基础设施层 - JPA 实体和适配器
// infrastructure/persistence/OrderJpaEntity.java
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id
private UUID id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private BigDecimal totalAmount;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItemJpaEntity> items;
}
// infrastructure/persistence/OrderRepositoryAdapter.java
@Component
@RequiredArgsConstructor
public class OrderRepositoryAdapter implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderJpaMapper mapper;
@Override
public Order save(Order order) {
OrderJpaEntity entity = mapper.toEntity(order);
return mapper.toDomain(jpaRepository.save(entity));
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepository.findById(id.value()).map(mapper::toDomain);
}
}
示例 6:适配器层 - REST 控制器
// adapter/rest/OrderController.java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderResponse response = createOrderUseCase.createOrder(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(response.id())
.toUri();
return ResponseEntity.created(location).body(response);
}
}
示例 7:领域测试(不加载 Spring 上下文)
class OrderTest {
@Test
void shouldCreateOrderWithValidItems() {
List<OrderItem> items = List.of(
new OrderItem(new ProductId(UUID.randomUUID()), 2, new Money("10.00", EUR))
);
Order order = Order.create(items);
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
assertThat(order.getDomainEvents()).hasSize(1);
}
}
示例 8:应用测试(使用模拟对象的单元测试)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository orderRepository;
@Mock PaymentGateway paymentGateway;
@Mock DomainEventPublisher eventPublisher;
@InjectMocks OrderService orderService;
@Test
void shouldCreateAndConfirmOrder() {
when(paymentGateway.charge(any())).thenReturn(new PaymentResult(true, "tx-123"));
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
OrderResponse response = orderService.createOrder(createRequest());
assertThat(response.status()).isEqualTo(OrderStatus.CONFIRMED);
verify(eventPublisher).publish(any(OrderCreatedEvent.class));
}
}
最佳实践
- 领域纯粹性:让领域层不包含 Spring 注解和框架导入——不依赖外层
- 按功能组织包:按业务能力(
order/、customer/)而非技术角色组织,每个功能都包含全部四层 - 不可变值对象:使用 Java record 创建值对象,并在紧凑构造器中内置校验——从设计上保证不可变
- 富领域模型:将业务逻辑放在实体和聚合中,而不是应用服务中——服务负责编排,实体负责封装
- 始终进行映射:使用 MapStruct 或手动映射器将 JPA 实体与领域模型分开;绝不在基础设施层之外暴露 JPA 实体
- 使用领域事件实现解耦:使用
DomainEventPublisher解耦跨聚合副作用,而不是直接调用服务 - 在应用层划定事务边界:只在应用服务上放置
@Transactional,绝不放在领域类上 - 使用工厂方法确保不变量:使用
Entity.create(...)静态方法,在构造时确保不变量 - 使用 ArchUnit 强制执行:在测试套件中添加 ArchUnit 测试,验证 Spring 或基础设施导入不会进入领域层
- 强类型 ID:使用
record OrderId(UUID value),而不是原始UUID,以避免聚合间 ID 混淆
约束和警告
关键约束
- 领域层纯粹性:绝不在领域类中添加 Spring 注解(
@Entity、@Autowired、@Component) - 依赖方向:依赖只能向内指向(领域层 <- 应用层 <- 基础设施层/适配器层)
- 框架隔离:所有特定于框架的代码都必须留在基础设施层和适配器层
应避免的常见陷阱
- 贫血领域模型:实体只有 getter/setter,逻辑却放在服务中——应将业务逻辑放在实体中
- 框架泄漏:在领域层使用
@Entity、@Autowired——保持领域层不依赖框架 - 延迟加载问题:通过领域模型暴露 JPA 实体——使用映射器进行转换
- 循环依赖:领域聚合之间存在循环依赖——使用 ID 而非直接引用
- 缺少领域事件:跨聚合通信时直接调用服务,而不是使用事件
- 仓储位置不当:在基础设施层定义仓储接口——仓储接口应属于领域层
- 绕过 DTO:在 API 中直接暴露领域实体——对外部契约始终使用 DTO
性能注意事项
- 将 JPA 实体与领域模型分开,以避免延迟加载问题
- 查询操作使用只读事务
- 对于复杂的读写场景,可考虑 CQRS
参考资料
references/java-clean-architecture.md- Java 专属模式(records、sealed classes、强类型 ID)references/spring-boot-implementation.md- Spring Boot 集成(DI 模式、JPA 映射、事务管理)
评论
支持 Markdown