ArchUnit 深度实践:把架构规范写成可以自动执行的测试

欢迎你来读这篇博客,这篇博客主要介绍 Java 架构测试工具 ArchUnit

它不是用来测试某个接口是否返回正确结果,也不是替代 JUnit、Mockito、Checkstyle、SpotBugs 或 SonarQube;它真正解决的是一个更隐蔽但更致命的问题:
项目一开始设计得很好,几个月后依赖关系乱成一锅粥,Controller 直接调 Mapper,Domain 依赖 Spring,Application 依赖
Infrastructure,最后架构图只剩下 PPT 里的体面。

ArchUnit 的价值就是:把“架构规范”写成自动化测试,让架构约束进入 CI 流水线。功能测试保证业务没坏,ArchUnit 保证架构没塌。

序言

很多团队都有架构规范,比如:

  • Controller 不能直接访问 Repository / Mapper;
  • Domain 层不能依赖 Spring、JPA、MyBatis、Redis 等技术框架;
  • Application 层只能编排用例,不能写基础设施细节;
  • Infrastructure 可以实现 Domain 定义的接口,但 Domain 不能反向依赖 Infrastructure;
  • 不同业务模块之间不能乱互相依赖;
  • 包之间不能出现循环依赖;
  • 类命名、注解使用、包结构必须符合约定。

问题是,这些规范如果只写在文档里,基本会经历下面的生命周期:

flowchart LR
    A[架构规范写进文档] --> B[新人没看]
    B --> C[老员工赶需求也没空看]
    C --> D[Code Review 偶尔发现]
    D --> E[线上需求一急先合并]
    E --> F[架构开始腐化]
    F --> G[重构成本暴涨]

这不是某个程序员“不自觉”,而是人肉约束本来就靠不住。架构约束如果不能自动化执行,最终就会变成“建议”。而建议,在 deadline
面前,一般都比较脆弱。

ArchUnit 的核心思想很简单:

架构规则不要只写在文档里,而要写成测试。

比如下面这条规则:

1
controller 包中的类不能依赖 repository 包中的类。

用 ArchUnit 就可以写成:

1
2
3
4
5
@ArchTest
static final ArchRule controllers_should_not_access_repositories =
noClasses()
.that().resideInAPackage("..controller..")
.should().dependOnClassesThat().resideInAPackage("..repository..");

以后只要有人在 Controller 里直接注入 Repository,测试就会失败。CI 一红,比架构师在群里发 800 字小作文更有效。

一、ArchUnit 是什么?

ArchUnit 是一个 Java 架构测试库。它会分析已经编译好的 Java 字节码,把类、方法、字段、注解、包、依赖关系等信息导入成 Java
对象,然后允许我们用 Java 代码编写架构规则。

它可以检查:

  • 包与包之间的依赖关系;
  • 类与类之间的依赖关系;
  • 分层架构是否被破坏;
  • Onion / Hexagonal Architecture 是否被破坏;
  • 包之间是否存在循环依赖;
  • 类名、包名、注解是否符合规范;
  • 某些层是否错误依赖了框架类;
  • 某些类是否被错误地访问;
  • 老项目中是否新增了架构违规。

它的使用方式也很接地气:就是写测试类。

你可以把 ArchUnit 测试放在:

1
src/test/java

然后像普通单元测试一样通过 Maven、Gradle、IDE、CI 执行。

二、ArchUnit 适合解决什么问题?

1. 防止分层架构被绕过

典型 Spring Boot 项目可能是这样:

1
controller -> service -> repository

或者更工程化一点:

1
2
adapter -> application -> domain
infrastructure -> domain

但是项目写着写着,可能变成:

flowchart TB
    Controller --> ApplicationService
    ApplicationService --> DomainService
    DomainService --> DomainModel
    Infrastructure --> DomainRepository
    Controller -. 错误依赖 .-> Mapper
    Controller -. 错误依赖 .-> Repository
    DomainModel -. 错误依赖 .-> Spring
    ApplicationService -. 错误依赖 .-> JpaRepository

这些问题如果只靠 Code Review,漏掉是迟早的。ArchUnit 可以把这些“不能这样依赖”的规则固化下来。

2. 防止 Domain 层被技术框架污染

DDD、COLA、六边形架构中,Domain 层应该表达业务概念和业务规则,而不是依赖 Spring、JPA、MyBatis、Redis、HTTP、MQ。

错误示例:

1
2
3
4
5
6
7
8
9
10
package com.example.order.domain.model;

import jakarta.persistence.Entity;
import org.springframework.beans.factory.annotation.Autowired;

@Entity
public class Order {
@Autowired
private SomeSpringService someSpringService;
}

这类代码短期看似方便,长期会让领域模型失去独立性。最后所谓的 Domain 层只剩下一个包名,里面全是框架注解和贫血对象。

ArchUnit 可以强制检查:

1
2
3
4
5
6
7
8
9
10
11
@ArchTest
static final ArchRule domain_should_not_depend_on_frameworks =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..",
"jakarta.persistence..",
"javax.persistence..",
"org.mybatis..",
"com.baomidou.mybatisplus.."
);

3. 防止循环依赖

循环依赖是系统腐化的重要信号。

比如:

1
order.service -> product.service -> stock.service -> order.service

短期内能跑,长期会造成:

  • 改一个模块牵一堆模块;
  • 单元测试很难隔离;
  • 业务边界越来越模糊;
  • 重构时不敢动;
  • 微服务拆分几乎无从下手。

ArchUnit 可以用 slices 规则检查循环依赖:

1
2
3
4
5
@ArchTest
static final ArchRule modules_should_be_free_of_cycles =
slices()
.matching("com.example.demo.(*)..")
.should().beFreeOfCycles();

4. 防止命名和注解混乱

比如团队约定:

  • Controller 必须放在 adapter.web 包下;
  • Controller 类必须以 Controller 结尾;
  • Application Service 必须以 AppService 结尾;
  • Repository 接口必须放在 domain.repository
  • Repository 实现必须放在 infrastructure.persistence
  • @RestController 只能出现在 Web Adapter 层。

这些也可以用 ArchUnit 测。

三、ArchUnit 和其他工具的区别

很多人第一次听 ArchUnit,会下意识问:这和 Checkstyle、PMD、SpotBugs、SonarQube 有什么区别?

简单说:

工具 主要关注点 典型问题
JUnit / Mockito 业务逻辑是否正确 某个方法返回值是否符合预期
Checkstyle 代码风格 缩进、命名、import 顺序
PMD 代码坏味道 空 catch、重复代码、复杂度过高
SpotBugs 潜在 Bug NPE、资源未关闭、错误比较
SonarQube 综合质量平台 Bug、漏洞、坏味道、覆盖率
ArchUnit 架构约束 层间依赖、循环依赖、包规则、架构边界

举个例子:

1
2
3
4
5
6
7
8
9
@RestController
public class OrderController {

private final OrderMapper orderMapper;

public OrderController(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
}

这段代码不一定有语法问题,也不一定有 bug,Checkstyle、SpotBugs 可能都觉得它“还行”。但是从架构角度看,Controller 直接依赖
Mapper,绕过了应用层,就是典型架构违规。

这正是 ArchUnit 擅长发现的问题。

四、ArchUnit 的核心工作原理

ArchUnit 的核心流程可以理解为三步:

flowchart LR
    A[编译后的 .class 字节码] --> B[ClassFileImporter 导入类]
    B --> C[JavaClasses / JavaClass 表示代码结构]
    C --> D[ArchRule 检查架构规则]
    D --> E[测试通过或失败]

1. ClassFileImporter

ClassFileImporter 用来导入被测项目的 class 文件。

1
2
JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.demo");

也可以排除测试代码和第三方 Jar:

1
2
3
4
JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
.importPackages("com.example.demo");

2. JavaClasses / JavaClass

导入后,ArchUnit 会把类信息抽象成 JavaClassesJavaClass 等对象。

这些对象能表示:

  • 类名;
  • 包名;
  • 父类;
  • 接口;
  • 注解;
  • 字段;
  • 方法;
  • 构造器;
  • 方法调用;
  • 字段访问;
  • 类之间的依赖关系。

3. ArchRule

ArchRule 是架构规则本身。

常见写法是:

1
2
3
classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

它读起来很像自然语言:

1
那些位于 service 包中的类,应该只被 controller 包和 service 包访问。

五、快速开始:在 Spring Boot 项目中接入 ArchUnit

下面以 Spring Boot 3.x + JUnit 5 + Maven 为例。

1. Maven 依赖

1
2
3
4
5
6
7

<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>

如果你使用 Gradle:

1
2
3
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
}

archunit-junit5 是一个方便依赖,它会同时带上 JUnit 5 API 和运行时引擎。一般项目直接使用它就够了。

2. 创建第一个架构测试

假设项目包名是:

1
com.example.demo

创建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(
packages = "com.example.demo",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class ArchitectureTest {

@ArchTest
static final ArchRule controller_should_not_access_repository =
noClasses()
.that().resideInAPackage("..controller..")
.should().dependOnClassesThat().resideInAPackage("..repository..");
}

运行:

1
mvn test

如果有人写了:

1
2
3
4
5
6
7
8
9
@RestController
public class UserController {

private final UserRepository userRepository;

public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
}

测试就会失败。

六、实战案例:用 ArchUnit 保护 Spring Boot + DDD 项目架构

这一节给一个偏生产化的包结构,适合 Spring Boot 3.x、DDD、COLA 风格项目参考。

1. 目标架构

我们设计一个订单服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
com.example.order
├── OrderApplication.java
├── adapter
│ ├── web
│ │ ├── OrderController.java
│ │ └── request
│ │ └── CreateOrderRequest.java
│ └── messaging
│ └── OrderMessageConsumer.java
├── application
│ ├── command
│ │ └── CreateOrderCommand.java
│ └── service
│ └── OrderAppService.java
├── domain
│ ├── model
│ │ └── Order.java
│ ├── repository
│ │ └── OrderRepository.java
│ └── service
│ └── OrderDomainService.java
├── infrastructure
│ └── persistence
│ ├── entity
│ │ └── OrderEntity.java
│ ├── mapper
│ │ └── OrderMapper.java
│ └── repository
│ └── JpaOrderRepository.java
└── common
└── exception
└── BizException.java

架构依赖方向:

flowchart TB
    Adapter[adapter\nWeb / MQ / RPC 入站适配器] --> Application[application\n用例编排]
    Application --> Domain[domain\n领域模型 / 领域服务 / 仓储接口]
    Infrastructure[infrastructure\n数据库 / Redis / MQ / 第三方接口实现] --> Domain
    Infrastructure --> Common[common\n通用异常 / 工具]
    Application --> Common
    Adapter --> Common
    Domain -. 禁止 .-> Application
    Domain -. 禁止 .-> Infrastructure
    Domain -. 禁止 .-> Adapter
    Application -. 禁止 .-> Infrastructure
    Adapter -. 禁止 .-> Infrastructure

2. 核心代码示例

2.1 Domain Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.order.domain.model;

import java.math.BigDecimal;
import java.util.Objects;

public class Order {

private final Long id;
private final BigDecimal amount;

public Order(Long id, BigDecimal amount) {
if (amount == null || amount.signum() <= 0) {
throw new IllegalArgumentException("订单金额必须大于 0");
}
this.id = id;
this.amount = amount;
}

public Long getId() {
return id;
}

public BigDecimal getAmount() {
return amount;
}

public boolean isLargeOrder() {
return amount.compareTo(new BigDecimal("10000")) >= 0;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Order order)) {
return false;
}
return Objects.equals(id, order.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}

注意:这里没有 @Entity、没有 @Table、没有 @Service、没有 @Autowired。Domain 就老老实实表达业务,不要顺手把框架塞进来。

2.2 Domain Repository Port

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.order.domain.repository;

import com.example.order.domain.model.Order;

import java.util.Optional;

public interface OrderRepository {

void save(Order order);

Optional<Order> findById(Long orderId);
}

这是领域层定义的仓储接口,也可以理解为 Port。

2.3 Application Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.order.application.service;

import com.example.order.application.command.CreateOrderCommand;
import com.example.order.domain.model.Order;
import com.example.order.domain.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderAppService {

private final OrderRepository orderRepository;

public OrderAppService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

@Transactional
public Long createOrder(CreateOrderCommand command) {
Order order = new Order(command.orderId(), command.amount());
orderRepository.save(order);
return order.getId();
}
}

Application 层可以依赖 Domain 层的接口,但不能直接依赖 JpaRepositoryMapperEntityManager

2.4 Web Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.order.adapter.web;

import com.example.order.adapter.web.request.CreateOrderRequest;
import com.example.order.application.command.CreateOrderCommand;
import com.example.order.application.service.OrderAppService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/orders")
public class OrderController {

private final OrderAppService orderAppService;

public OrderController(OrderAppService orderAppService) {
this.orderAppService = orderAppService;
}

@PostMapping
public Long create(@RequestBody CreateOrderRequest request) {
CreateOrderCommand command = new CreateOrderCommand(request.orderId(), request.amount());
return orderAppService.createOrder(command);
}
}

Controller 只做协议适配,不直接访问数据库。

2.5 Infrastructure Repository Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.order.infrastructure.persistence.repository;

import com.example.order.domain.model.Order;
import com.example.order.domain.repository.OrderRepository;
import com.example.order.infrastructure.persistence.entity.OrderEntity;
import com.example.order.infrastructure.persistence.mapper.OrderMapper;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class JpaOrderRepository implements OrderRepository {

private final SpringDataOrderRepository springDataOrderRepository;
private final OrderMapper orderMapper;

public JpaOrderRepository(SpringDataOrderRepository springDataOrderRepository,
OrderMapper orderMapper) {
this.springDataOrderRepository = springDataOrderRepository;
this.orderMapper = orderMapper;
}

@Override
public void save(Order order) {
OrderEntity entity = orderMapper.toEntity(order);
springDataOrderRepository.save(entity);
}

@Override
public Optional<Order> findById(Long orderId) {
return springDataOrderRepository.findById(orderId)
.map(orderMapper::toDomain);
}
}

Infrastructure 可以依赖 Domain,因为它在实现 Domain 定义的端口。但 Domain 不能依赖 Infrastructure。

七、实战规则一:Controller 禁止直接访问 Repository / Mapper

1. 为什么要这么做?

Controller 直接访问 Repository 会带来几个问题:

  • 业务规则被写进 Controller;
  • 用例编排被绕过;
  • 事务边界混乱;
  • 权限、审计、日志等横切逻辑难统一;
  • 后续改协议、改数据库都会牵动入口层。

错误示例:

1
2
3
4
5
6
7
8
9
@RestController
public class OrderController {

private final SpringDataOrderRepository repository;

public OrderController(SpringDataOrderRepository repository) {
this.repository = repository;
}
}

2. ArchUnit 规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class ControllerDependencyArchTest {

@ArchTest
static final ArchRule controllers_should_not_access_persistence_directly =
noClasses()
.that().resideInAPackage("..adapter.web..")
.should().dependOnClassesThat().resideInAnyPackage(
"..domain.repository..",
"..infrastructure.persistence..",
"..infrastructure.persistence.repository..",
"..infrastructure.persistence.mapper.."
);
}

3. 解释

这条规则的意思是:

1
adapter.web 包中的类,不能依赖 domain.repository 和 infrastructure.persistence 包中的类。

为什么连 domain.repository 也禁止?

因为在 DDD / Clean Architecture 里,Controller 应该面向用例,而不是面向仓储接口。仓储接口虽然属于领域层,但它依然是数据访问抽象。Controller
如果直接调用仓储接口,本质上还是绕过了 Application 层。

八、实战规则二:Domain 层禁止依赖技术框架

1. 目标

Domain 层应该保持纯净,至少不应该依赖:

  • Spring Framework;
  • Spring Web;
  • Spring Data;
  • JPA / Hibernate;
  • MyBatis / MyBatis-Plus;
  • Redis;
  • Kafka / RabbitMQ;
  • Dubbo / gRPC;
  • Servlet API;
  • Infrastructure 包。

2. ArchUnit 规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class DomainPurityArchTest {

@ArchTest
static final ArchRule domain_should_not_depend_on_frameworks =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..",
"jakarta.persistence..",
"javax.persistence..",
"org.hibernate..",
"org.mybatis..",
"com.baomidou.mybatisplus..",
"org.springframework.data..",
"org.springframework.web..",
"jakarta.servlet..",
"javax.servlet..",
"org.apache.dubbo..",
"io.grpc..",
"org.springframework.kafka..",
"org.springframework.amqp..",
"org.springframework.data.redis.."
);

@ArchTest
static final ArchRule domain_should_not_depend_on_adapter_or_infrastructure =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..adapter..",
"..infrastructure.."
);
}

3. 推荐取舍

这条规则不是说 Domain 层不能使用任何第三方库。比如:

  • java.time 可以用;
  • BigDecimal 可以用;
  • 一些纯工具类可以谨慎使用;
  • 领域异常可以放在 domain 或 common。

真正要警惕的是:Domain 层依赖了某个技术实现细节。

比如:

1
2
3
@Entity
public class OrderEntityLikeDomainModel {
}

这会让领域对象和数据库模型绑死。以后你想换持久化方式、做领域测试、拆模块,都会被这些注解拖住。架构债就像利息,不会马上让你破产,但会稳定吸血。

九、实战规则三:用 layeredArchitecture 检查分层依赖

ArchUnit 内置了分层架构检查能力。

1. 规则代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class LayeredArchitectureArchTest {

@ArchTest
static final ArchRule layered_architecture_should_be_respected =
layeredArchitecture()
.consideringOnlyDependenciesInLayers()
.layer("Adapter").definedBy("..adapter..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")

.whereLayer("Adapter").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Adapter")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();
}

2. 规则含义

1
2
3
4
Adapter 层:外部入口层,不应该被其他层访问。
Application 层:只能被 Adapter 层访问。
Domain 层:可以被 Application 和 Infrastructure 访问。
Infrastructure 层:不应该被其他层访问。

为什么 Infrastructure 层不应该被 Application 访问?

因为 Application 应该依赖抽象,比如 OrderRepository,而不是依赖 JpaOrderRepositoryOrderMapperRedisTemplate
KafkaTemplate。否则依赖方向就反了。

3. consideringOnlyDependenciesInLayers 和 consideringAllDependencies 的区别

常见写法有两种:

1
2
layeredArchitecture()
.consideringOnlyDependenciesInLayers()

和:

1
2
layeredArchitecture()
.consideringAllDependencies()

区别是:

  • consideringOnlyDependenciesInLayers():主要检查你定义的这些层之间的依赖关系,实战中更容易上手;
  • consideringAllDependencies():更严格,会考虑所有依赖,可能需要额外忽略 JDK、第三方库、配置类等场景。

对大多数业务项目,建议先使用 consideringOnlyDependenciesInLayers(),等规则稳定后再逐步收紧。

十、实战规则四:用 onionArchitecture 检查六边形 / 洋葱架构

如果你的项目更接近 Clean Architecture、Hexagonal Architecture、Onion Architecture,可以使用 ArchUnit 提供的
onionArchitecture()

1. 目标模型

flowchart TB
    WebAdapter[Web Adapter] --> Application[Application Services]
    MessageAdapter[Message Adapter] --> Application
    PersistenceAdapter[Persistence Adapter] --> Application
    PersistenceAdapter --> Domain[Domain Model / Domain Service]
    Application --> Domain
    Domain -. 禁止依赖 .-> Application
    Domain -. 禁止依赖 .-> WebAdapter
    Domain -. 禁止依赖 .-> PersistenceAdapter
    Application -. 禁止依赖 .-> WebAdapter
    Application -. 禁止依赖 .-> PersistenceAdapter
    WebAdapter -. 禁止依赖 .-> PersistenceAdapter

2. 规则代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.Architectures.onionArchitecture;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class OnionArchitectureArchTest {

@ArchTest
static final ArchRule onion_architecture_should_be_respected =
onionArchitecture()
.domainModels("..domain.model..")
.domainServices("..domain.service..")
.applicationServices("..application..")
.adapter("web", "..adapter.web..")
.adapter("messaging", "..adapter.messaging..")
.adapter("persistence", "..infrastructure.persistence..");
}

3. layeredArchitecture 和 onionArchitecture 怎么选?

如果你项目是传统三层架构:

1
controller -> service -> repository

优先用 layeredArchitecture()

如果你项目强调:

1
2
3
4
domain 是核心
application 编排用例
adapter 负责外部交互
infrastructure 实现技术细节

可以考虑 onionArchitecture()

我的建议是:

  • 初期:用简单的 noClasses()layeredArchitecture()
  • 架构稳定后:再引入 onionArchitecture()
  • 老项目:不要一上来就拉满,否则测试一片红,团队会先把 ArchUnit 干掉,而不是干掉坏代码。

十一、实战规则五:检查包之间循环依赖

1. 按一级业务模块检查

假设包结构是:

1
2
3
4
com.example.order
com.example.product
com.example.stock
com.example.payment

可以写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

@AnalyzeClasses(
packages = "com.example",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class CycleDependencyArchTest {

@ArchTest
static final ArchRule business_modules_should_be_free_of_cycles =
slices()
.matching("com.example.(*)..")
.should().beFreeOfCycles();
}

2. 按某个应用内部模块检查

如果只想检查 order 服务内部:

1
2
3
4
5
@ArchTest
static final ArchRule order_internal_packages_should_be_free_of_cycles =
slices()
.matching("com.example.order.(*)..")
.should().beFreeOfCycles();

3. 循环依赖的典型坏味道

比如下面这种:

1
2
3
order.application -> product.application
product.application -> stock.application
stock.application -> order.application

往往说明模块边界没设计好。通常有几种修复方向:

  • 抽出一个更上层的应用编排服务;
  • 把跨模块依赖改成领域事件;
  • 抽出公共能力到 shared kernel;
  • 通过接口反转依赖;
  • 拆清楚查询依赖和命令依赖;
  • 避免模块之间互相调用彼此的内部 service。

十二、实战规则六:命名规范和注解规范

ArchUnit 不只是查依赖,也能查命名和注解。

1. Controller 必须放在 adapter.web 包下,并以 Controller 结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.web.bind.annotation.RestController;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class NamingArchTest {

@ArchTest
static final ArchRule rest_controllers_should_be_named_and_located_correctly =
classes()
.that().areAnnotatedWith(RestController.class)
.should().resideInAPackage("..adapter.web..")
.andShould().haveSimpleNameEndingWith("Controller");
}

2. Application Service 必须放在 application.service 包下

1
2
3
4
5
@ArchTest
static final ArchRule app_services_should_be_located_correctly =
classes()
.that().haveSimpleNameEndingWith("AppService")
.should().resideInAPackage("..application.service..");

3. Repository 接口必须放在 domain.repository 包下

1
2
3
4
5
@ArchTest
static final ArchRule domain_repositories_should_be_interfaces =
classes()
.that().resideInAPackage("..domain.repository..")
.should().beInterfaces();

4. Spring Repository 实现必须放在 infrastructure.persistence.repository 包下

1
2
3
4
5
6
7
import org.springframework.stereotype.Repository;

@ArchTest
static final ArchRule spring_repositories_should_be_located_in_infrastructure =
classes()
.that().areAnnotatedWith(Repository.class)
.should().resideInAPackage("..infrastructure.persistence.repository..");

这类规则看起来简单,但非常适合团队统一工程结构。尤其是多人协作、多个微服务复制模板时,可以避免“每个人都有自己的包结构美学”。

十三、实战规则七:禁止使用字段注入

Spring 项目中经常会约定:禁止字段注入,统一使用构造器注入。

错误示例:

1
2
3
4
5
6
@Service
public class OrderAppService {

@Autowired
private OrderRepository orderRepository;
}

ArchUnit 规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.beans.factory.annotation.Autowired;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class DependencyInjectionArchTest {

@ArchTest
static final ArchRule fields_should_not_be_injected =
noFields()
.should().beAnnotatedWith(Autowired.class);
}

建议使用构造器注入:

1
2
3
4
5
6
7
8
9
@Service
public class OrderAppService {

private final OrderRepository orderRepository;

public OrderAppService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}

如果你用 Lombok,也可以:

1
2
3
4
5
6
@Service
@RequiredArgsConstructor
public class OrderAppService {

private final OrderRepository orderRepository;
}

十四、实战规则八:禁止 Application 层直接依赖 Mapper / JpaRepository

1. 错误示例

1
2
3
4
5
6
@Service
public class OrderAppService {

private final OrderMapper orderMapper;
private final SpringDataOrderRepository repository;
}

这说明 Application 层已经依赖了持久化实现。

2. 正确示例

1
2
3
4
5
@Service
public class OrderAppService {

private final OrderRepository orderRepository;
}

其中 OrderRepository 是 Domain 层定义的接口。

3. ArchUnit 规则

1
2
3
4
5
6
7
8
9
10
@ArchTest
static final ArchRule application_should_not_depend_on_persistence_implementation =
noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat().resideInAnyPackage(
"..infrastructure.persistence..",
"org.springframework.data.jpa.repository..",
"com.baomidou.mybatisplus..",
"org.mybatis.."
);

这条规则很重要。因为很多项目所谓“DDD 分层”最后失败,就是因为 Application 层直接拿着各种 Mapper 到处查数据库。

十五、实战规则九:限制模块之间只能通过 API 包访问

在多模块或大单体项目里,业务模块之间经常互相调用。

比如:

1
order 模块想访问 product 模块

比较好的方式是只允许访问 product 暴露出来的 API 包:

1
com.example.product.api

不允许访问:

1
2
3
com.example.product.application
com.example.product.domain
com.example.product.infrastructure

1. 包结构

1
2
3
4
5
6
7
8
9
10
11
12
13
com.example
├── order
│ ├── application
│ └── domain
├── product
│ ├── api
│ ├── application
│ ├── domain
│ └── infrastructure
└── stock
├── api
├── application
└── domain

2. 规则示例

1
2
3
4
5
6
7
8
9
@ArchTest
static final ArchRule order_should_only_access_product_api =
noClasses()
.that().resideInAPackage("com.example.order..")
.should().dependOnClassesThat().resideInAnyPackage(
"com.example.product.application..",
"com.example.product.domain..",
"com.example.product.infrastructure.."
);

更通用一点,可以按照团队模块边界继续抽象。但不建议一开始写太复杂的动态规则,先把最痛的几个模块边界管起来。

十六、实战规则十:多模块 Maven 项目如何落地?

假设你的 Maven 项目是这样:

1
2
3
4
5
6
finance-parent
├── finance-order
├── finance-product
├── finance-settlement
├── finance-common
└── finance-architecture-test

有两种方案。

方案一:每个业务模块自己写 ArchUnit 测试

优点:

  • 规则离业务模块近;
  • 执行简单;
  • 失败定位清晰。

缺点:

  • 多个模块可能复制规则;
  • 全局规则不容易统一。

适合中小项目。

方案二:单独建立 architecture-test 模块

把全局架构规则集中到:

1
finance-architecture-test

这个模块依赖其他业务模块,然后统一扫描:

1
2
3
4
5
6
7
8
9
@AnalyzeClasses(
packages = "com.company.finance",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class FinanceArchitectureTest {
}

优点:

  • 架构规则统一管理;
  • 适合平台组或架构组维护;
  • 可以沉淀公司级架构规范。

缺点:

  • 模块依赖配置更复杂;
  • 扫描范围大时测试可能更慢;
  • 规则过严时容易影响所有模块。

我的建议:

1
2
3
小项目:每个模块自己写。
中大型项目:公共规则抽成测试工具包,各模块复用。
平台型项目:单独 architecture-test 模块集中治理。

十七、老项目如何引入 ArchUnit?不要一上来就核弹洗地

老项目最怕什么?

不是没有规范,而是一加规范,测试全红。

比如你一口气加了:

  • Domain 不许依赖 Spring;
  • Controller 不许访问 Repository;
  • 包不能循环依赖;
  • 模块不能互相依赖内部实现;
  • 字段注入禁止;
  • 命名规范强制。

结果一跑:

1
Architecture Violation [Priority: MEDIUM] - Rule ... was violated (729 times)

这个时候大概率会发生:

1
2
团队:ArchUnit 不适合我们项目。
真相:不是不适合,是你第一天就让它当灭霸。

1. 老项目推荐引入顺序

flowchart TB
    A[第一阶段:只检查新增代码最容易遵守的规则] --> B[第二阶段:检查命名和包位置]
B --> C[第三阶段:禁止新增 Controller 直连 Mapper]
C --> D[第四阶段:治理循环依赖]
D --> E[第五阶段:收紧 Domain 纯净度]
E --> F[第六阶段:沉淀公司级架构规则]

2. 使用 FreezingArchRule 冻结历史违规

ArchUnit 提供了 FreezingArchRule,适合老项目渐进式治理。

它的思路是:

1
历史违规先记录下来,以后只拦截新增违规。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.freeze.FreezingArchRule.freeze;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class FrozenArchitectureTest {

@ArchTest
static final ArchRule frozen_domain_should_not_depend_on_spring =
freeze(
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("org.springframework..")
);
}

添加配置文件:

1
2
3
4
# src/test/resources/archunit.properties
freeze.store.default.path=src/test/resources/archunit/frozen
freeze.store.default.allowStoreCreation=true
freeze.store.default.allowStoreUpdate=true

第一次运行会生成冻结记录。

提交冻结文件后,在 CI 环境可以改成:

1
2
freeze.store.default.allowStoreCreation=false
freeze.store.default.allowStoreUpdate=false

这样 CI 不会偷偷创建或更新冻结记录,只会拦截新增违规。

十八、archunit.properties 推荐配置

可以在:

1
src/test/resources/archunit.properties

增加配置。

1. 推荐基础配置

1
2
3
4
5
6
# 让 ArchUnit 测试名中的下划线显示为空格,测试报告更友好
junit.displayName.replaceUnderscoresBySpaces=true

# 控制循环依赖最多报告多少条,避免超大项目输出爆炸
cycles.maxNumberToDetect=50
cycles.maxNumberOfDependenciesPerEdge=5

2. 性能优化配置

如果项目很大,ArchUnit 解析缺失依赖很慢,可以考虑:

1
resolveMissingDependenciesFromClassPath=false

这会减少从 classpath 继续解析缺失类的成本。但如果你的规则依赖很深的第三方类型信息,需要谨慎使用。

3. 不建议轻易关闭空规则失败

ArchUnit 默认会在规则没有匹配到任何类时失败,这其实是好事。

比如你的规则是:

1
classes().that().resideInAPackage("..service..").should()...

后来包名改成了 application.service,如果空规则也算通过,那么测试就变成“测了,但没完全测”。这比不测还迷惑。

所以不建议全局设置:

1
archRule.failOnEmptyShould=false

除非你非常确定某些模块暂时没有对应包。

十九、CI 集成

ArchUnit 本质是测试,所以 CI 接入非常简单。

1. Maven 命令

1
mvn test

只运行架构测试:

1
mvn test -Dtest=*ArchTest

或者:

1
mvn test -Dtest=*ArchitectureTest

2. GitHub Actions 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
name: Java CI

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Run tests
run: mvn -B test

3. Jenkins Pipeline 示例

1
2
3
4
5
6
7
8
9
10
11
pipeline {
agent any

stages {
stage('Test') {
steps {
sh 'mvn -B test'
}
}
}
}

4. 推荐流水线策略

1
2
3
4
PR 阶段:必须运行 ArchUnit。
主干阶段:必须运行 ArchUnit。
每日构建:可以运行更严格、更慢的全量架构规则。
老项目治理阶段:新增违规必须阻断,历史违规逐步消化。

二十、完整可复制的 ArchitectureTest 模板

下面给一个可以直接复制到 Spring Boot + DDD 项目的模板。

你只需要把包名 com.example.order 改成自己的根包名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package com.example.order.arch;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.RestController;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

@AnalyzeClasses(
packages = "com.example.order",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
class ArchitectureTest {

@ArchTest
static final ArchRule layered_architecture_should_be_respected =
layeredArchitecture()
.consideringOnlyDependenciesInLayers()
.layer("Adapter").definedBy("..adapter..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infrastructure..")
.whereLayer("Adapter").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Adapter")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();

@ArchTest
static final ArchRule onion_architecture_should_be_respected =
onionArchitecture()
.domainModels("..domain.model..")
.domainServices("..domain.service..")
.applicationServices("..application..")
.adapter("web", "..adapter.web..")
.adapter("messaging", "..adapter.messaging..")
.adapter("persistence", "..infrastructure.persistence..");

@ArchTest
static final ArchRule controllers_should_not_access_persistence_directly =
noClasses()
.that().resideInAPackage("..adapter.web..")
.should().dependOnClassesThat().resideInAnyPackage(
"..domain.repository..",
"..infrastructure.persistence..",
"..infrastructure.persistence.repository..",
"..infrastructure.persistence.mapper.."
);

@ArchTest
static final ArchRule application_should_not_depend_on_infrastructure =
noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat().resideInAnyPackage(
"..infrastructure..",
"org.springframework.data.jpa.repository..",
"org.mybatis..",
"com.baomidou.mybatisplus.."
);

@ArchTest
static final ArchRule domain_should_not_depend_on_frameworks =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..",
"jakarta.persistence..",
"javax.persistence..",
"org.hibernate..",
"org.mybatis..",
"com.baomidou.mybatisplus..",
"jakarta.servlet..",
"javax.servlet..",
"org.apache.dubbo..",
"io.grpc.."
);

@ArchTest
static final ArchRule domain_should_not_depend_on_adapter_or_infrastructure =
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..adapter..",
"..infrastructure.."
);

@ArchTest
static final ArchRule modules_should_be_free_of_cycles =
slices()
.matching("com.example.order.(*)..")
.should().beFreeOfCycles();

@ArchTest
static final ArchRule rest_controllers_should_be_named_and_located_correctly =
classes()
.that().areAnnotatedWith(RestController.class)
.should().resideInAPackage("..adapter.web..")
.andShould().haveSimpleNameEndingWith("Controller");

@ArchTest
static final ArchRule app_services_should_be_located_correctly =
classes()
.that().haveSimpleNameEndingWith("AppService")
.should().resideInAPackage("..application.service..");

@ArchTest
static final ArchRule domain_repositories_should_be_interfaces =
classes()
.that().resideInAPackage("..domain.repository..")
.should().beInterfaces();

@ArchTest
static final ArchRule spring_repositories_should_be_located_in_infrastructure =
classes()
.that().areAnnotatedWith(Repository.class)
.should().resideInAPackage("..infrastructure.persistence.repository..");

@ArchTest
static final ArchRule fields_should_not_be_injected =
noFields()
.should().beAnnotatedWith(Autowired.class);
}

注意

这个模板不是让你一口气全部启用。建议按项目情况拆分为:

1
2
3
4
5
LayeredArchitectureArchTest
DomainPurityArchTest
NamingArchTest
CycleDependencyArchTest
DependencyInjectionArchTest

这样失败时更容易定位,也更利于团队逐步治理。

二十一、ArchUnit 规则设计原则

1. 先约束最痛的地方

不要为了“架构洁癖”写一堆没人理解的规则。

优先治理:

  • Controller 直连 Mapper;
  • Domain 依赖 Spring;
  • Application 依赖 Infrastructure;
  • 模块循环依赖;
  • 字段注入;
  • 关键包命名混乱。

这些都是高收益规则。

2. 规则要能解释业务价值

不要只说:

1
因为架构规范要求这样。

要说明:

1
2
为什么 Controller 不能直接访问 Mapper?
因为这会绕过用例编排,导致事务、权限、审计、日志、校验散落到入口层。

团队能理解,规则才推得动。

3. 不要过度约束

坏规则比没规则更可怕。

比如有的团队写:

1
2
3
所有类必须以固定后缀结尾。
所有包必须精确匹配某个模板。
所有 service 只能被 controller 访问。

结果遇到异步消费者、定时任务、内部事件监听器,规则马上开始误伤。

ArchUnit 是扳手,不是尚方宝剑。拧螺丝很好用,拿来拍核桃也不是不行,但多少有点不讲武德。

4. 规则失败信息要可读

建议规则名写清楚:

1
controllers_should_not_access_persistence_directly

而不是:

1
rule1

测试失败时,团队能第一眼看懂违反了什么。

5. 对老项目要渐进式治理

老项目推荐策略:

1
2
3
4
5
新增违规禁止。
历史违规冻结。
每次迭代消化一部分。
关键模块优先治理。
架构规则进入 PR 必跑。

二十二、常见问题 FAQ

1. ArchUnit 会不会很慢?

小中型项目通常很快。大型项目扫描全 classpath 时可能会慢一些。

优化方式:

  • 只扫描项目根包,不扫描全 classpath;
  • 使用 DoNotIncludeTests
  • 使用 DoNotIncludeJars
  • 不要每条规则都手动 new ClassFileImporter,优先使用 JUnit 5 的 @AnalyzeClasses 缓存;
  • 大项目拆分规则和扫描范围。

2. ArchUnit 能检查运行时依赖吗?

ArchUnit 主要分析静态字节码依赖。它适合检查编译期可见的结构关系。

例如:

  • A 类字段依赖 B 类;
  • A 类方法调用 B 类方法;
  • A 类注解引用某个注解;
  • A 类继承或实现某个类型。

但如果你通过字符串、反射、配置文件动态调用某个类,ArchUnit 不一定能知道。

3. ArchUnit 能替代 Code Review 吗?

不能。

ArchUnit 适合检查稳定、明确、可自动化的规则。Code Review 仍然需要关注:

  • 业务语义是否正确;
  • 代码可读性;
  • 抽象是否合理;
  • 复杂逻辑是否清晰;
  • 性能和安全问题;
  • 边界条件。

ArchUnit 是帮你把重复性的架构检查自动化,让 Code Review 有精力看更有价值的东西。

4. ArchUnit 和 SonarQube 冲突吗?

不冲突。

SonarQube 更偏综合质量平台,ArchUnit 更偏架构约束测试。两者可以同时使用。

5. 包名变了怎么办?

ArchUnit 规则通常依赖包结构,所以包结构调整时需要同步调整规则。

建议:

  • 包结构不要频繁变;
  • 架构测试类集中管理;
  • 使用 packagesOf = SomeRootClass.class 减少根包硬编码;
  • 规则命名清晰,便于修改。

例如:

1
2
3
4
5
6
@AnalyzeClasses(
packagesOf = OrderApplication.class,
importOptions = ImportOption.DoNotIncludeTests.class
)
class ArchitectureTest {
}

二十三、生产落地建议

1. 新项目推荐规则集

新项目可以直接启用:

1
2
3
4
5
6
7
1. 分层依赖规则
2. Domain 纯净度规则
3. Controller 禁止访问 Repository / Mapper
4. Application 禁止访问 Infrastructure
5. 循环依赖检查
6. 字段注入禁止
7. Controller / Service / Repository 命名和包位置规则

2. 老项目推荐规则集

老项目建议先启用:

1
2
3
4
5
1. 字段注入禁止新增
2. Controller 禁止新增直连 Mapper
3. 新代码包命名规范
4. 新增循环依赖阻断
5. FreezingArchRule 冻结历史违规

3. 微服务项目推荐策略

微服务项目里,可以沉淀一个公司级测试工具包:

1
company-architecture-test-starter

里面提供:

  • 标准分层规则;
  • DDD 规则;
  • Spring Boot 规则;
  • 命名规范;
  • 禁止字段注入;
  • 禁止 Controller 直连 Mapper;
  • 禁止 Domain 依赖框架;
  • 允许各服务扩展自己的规则。

每个服务只需要引入测试依赖:

1
2
3
4
5
6
7

<dependency>
<groupId>com.company</groupId>
<artifactId>company-architecture-test-starter</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>

然后写:

1
2
3
@CompanyArchitectureTest(rootPackage = "com.company.finance.order")
class OrderArchitectureTest {
}

这个属于更高级的封装,可以后续逐步建设。

二十四、总结

ArchUnit 的核心价值不是“多写几个测试”,而是把架构规则从口头约定、文档约定、Code Review 经验,升级成可以自动执行的工程约束。

它特别适合:

  • Spring Boot 中大型项目;
  • DDD / COLA / Clean Architecture 项目;
  • 多模块 Maven 项目;
  • 微服务项目;
  • 老项目架构治理;
  • 团队多人协作、代码边界容易失控的项目。

一句话总结:

JUnit 保证功能没坏,ArchUnit 保证架构没塌。

当项目越来越大时,真正可怕的不是某个接口有 bug,而是系统边界不断被侵蚀,最后每个类都能调每个类,每个包都能依赖每个包。那时候再谈重构,就像在毛线团里找耳机线,能找到,但不一定还能保持情绪稳定。

所以,如果你的项目已经开始出现:

  • Controller 直连 Mapper;
  • Domain 依赖 Spring;
  • Application 依赖 Infrastructure;
  • 包循环依赖;
  • 模块边界不清;
  • 架构规范只存在于文档中;

那 ArchUnit 很值得接进来。

不用一上来写几十条规则,先从最痛的一两条开始。让架构规则进入测试,进入 CI,进入团队日常开发流程。架构治理这件事,越早自动化,越少靠吼。

参考资料


ArchUnit 深度实践:把架构规范写成可以自动执行的测试
https://allendericdalexander.github.io/2026/06/12/archtect/arch/archunit-blog/
作者
AtLuoFu
发布于
2026年6月12日
许可协议