1. 概述
本文将介绍如何使用 ArchUnit 对系统架构进行校验。
在软件工程领域,架构设计与系统可维护性之间的关系早已被广泛研究。仅仅定义一套清晰的架构是不够的——更重要的是,确保代码实现真正遵循了这套架构。
ArchUnit 正是为此而生:它是一个用于验证代码是否符合预设架构规则的测试库。
接下来,我们将从零开始,带你掌握 ArchUnit 的核心用法,避免在实际项目中踩坑。
2. 什么是 ArchUnit?
简单来说,ArchUnit 是一个 Java 测试库,允许我们通过代码定义并验证架构约束,并在构建过程中与其他单元测试一并执行。
但问题来了:这里说的“架构”到底指什么?所谓的“架构规则”又是什么?
什么是架构?
在 ArchUnit 的语境中,“架构”主要指项目中类的组织方式,尤其是包(package)的划分与依赖关系。
更进一步,架构还定义了不同层级(layer)之间的调用规则。例如,典型的三层架构:
- ✅ Presentation(表现层):如 Controller
- ✅ Service(业务层):处理核心逻辑
- ✅ Persistence(持久层):如 Repository,负责数据访问
它们之间的依赖应该是单向的:
Presentation → Service → Persistence
我们可以通过 UML 包图来可视化这种结构:
从图中可以提炼出以下规则:
- ❌ Presentation 层不能直接依赖 Persistence 层
- ❌ Service 层不能依赖 Presentation 层
- ❌ Persistence 层不能反向依赖其他层
这些,就是典型的“架构规则”——它本质上是对类之间调用关系的断言。
而 ArchUnit 的作用,就是把这些规则写成可执行的测试,防止团队成员无意中破坏架构。
3. 项目集成
ArchUnit 与 JUnit 深度集成,使用非常方便。根据你使用的 JUnit 版本选择对应依赖即可。
使用 JUnit 4
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
使用 JUnit 5(推荐)
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
⚠️ 注意:虽然两个 artifactId 不同,但核心 API 是一致的。推荐新项目直接使用 JUnit 5 版本。
4. 编写 ArchUnit 测试
假设我们有一个简单的 Spring Boot 项目,包含以下结构:
com.example.smurfs
├── presentation
│ └── SmurfController.java
├── service
│ └── SmurfService.java
└── persistence
└── SmurfRepository.java
目标:确保各层之间依赖不越界。
4.1 第一个测试:限制 Presentation 层依赖
第一步,加载所有待检测的类:
JavaClasses jc = new ClassFileImporter()
.importPackages("com.baeldung.archunit.smurfs");
JavaClasses
对象就是我们的“检查目标”,类似于单元测试中的被测对象。
接下来,定义规则:Presentation 层只能依赖 Service 层:
ArchRule r1 = classes()
.that().resideInAPackage("..presentation..")
.should().onlyDependOnClassesThat()
.resideInAPackage("..service..");
r1.check(jc);
运行后,大概率会失败:
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] -
Rule 'classes that reside in a package '..presentation..' should only
depend on classes that reside in a package '..service..'' was violated (6 times):
... error list omitted
❌ 踩坑点:JVM 和框架类也被视为“依赖”
onlyDependOnClassesThat()
是严格白名单机制。但实际中,Controller 必然会用到:
java.lang.String
org.springframework.web.bind.annotation.GetMapping
javax.servlet.http.HttpServletRequest
这些来自 java..
、javax..
、org.springframework..
的类都会触发违规。
4.2 改进方案:使用 deny-based 规则
与其列出所有允许的包(维护成本高),不如换个思路:禁止某些依赖。
ArchRule r1 = noClasses()
.that().resideInAPackage("..presentation..")
.should().dependOnClassesThat()
.resideInAPackage("..persistence..");
✅ 这种写法更简洁、更健壮:
- 允许 Presentation 层调用任何类(包括 JDK、Spring)
- 仅禁止其直接依赖 Persistence 层
这就是所谓的 deny-based(拒绝式)规则,相比 allow-based(允许式)更实用,也更符合实际开发场景。
💡 小技巧:ArchUnit 的 fluent API 非常灵活,同一规则可用多种方式表达,选择最清晰、最易维护的即可。
5. 使用 Library API 快速构建复杂规则
ArchUnit 内置了 Library API
,提供一系列开箱即用的高级规则,大幅提升开发效率。
常见内置模块
模块 | 用途 |
---|---|
Architectures |
支持分层架构、洋葱架构等 |
Slices |
检测包间循环依赖(cyclic dependencies) |
General |
日志、异常处理等编码规范 |
PlantUML |
校验代码是否符合 UML 设计图 |
Freeze Arch Rules |
冻结现有违规,仅报告新增问题(适合治理技术债) |
我们以 layeredArchitecture
为例,重写之前的三层依赖规则:
LayeredArchitecture arch = layeredArchitecture()
// 定义层及其包路径
.layer("Presentation").definedBy("..presentation..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
// 定义访问规则
.whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
arch.check(jc);
✅ 优势
- 简单粗暴,几行代码搞定整个架构约束
- 可读性强,规则一目了然
- 自动处理 JDK/Spring 等框架依赖,无需手动排除
⚠️ 注意:
layeredArchitecture()
来自com.tngtech.archunit.library.Architectures
,需静态导入。
6. 总结
ArchUnit 是一个轻量但强大的架构守护工具,能有效防止代码腐化。它的核心价值在于:
- ✅ 将架构规则变成可执行的测试
- ✅ 与 CI/CD 集成,实现“架构即代码”
- ✅ 提供高阶 API,降低规则编写成本
对于中大型项目,建议尽早引入 ArchUnit,避免后期架构失控。
所有示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/libraries-testing