1. 概述

本文将介绍如何使用 ArchUnit 对系统架构进行校验。

在软件工程领域,架构设计与系统可维护性之间的关系早已被广泛研究。仅仅定义一套清晰的架构是不够的——更重要的是,确保代码实现真正遵循了这套架构。
ArchUnit 正是为此而生:它是一个用于验证代码是否符合预设架构规则的测试库

接下来,我们将从零开始,带你掌握 ArchUnit 的核心用法,避免在实际项目中踩坑。


2. 什么是 ArchUnit?

简单来说,ArchUnit 是一个 Java 测试库,允许我们通过代码定义并验证架构约束,并在构建过程中与其他单元测试一并执行。

但问题来了:这里说的“架构”到底指什么?所谓的“架构规则”又是什么?

什么是架构?

在 ArchUnit 的语境中,“架构”主要指项目中类的组织方式,尤其是包(package)的划分与依赖关系

更进一步,架构还定义了不同层级(layer)之间的调用规则。例如,典型的三层架构:

  • Presentation(表现层):如 Controller
  • Service(业务层):处理核心逻辑
  • Persistence(持久层):如 Repository,负责数据访问

它们之间的依赖应该是单向的:

Presentation → Service → Persistence

我们可以通过 UML 包图来可视化这种结构:

figure1-1

从图中可以提炼出以下规则:

  • ❌ 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


原始标题:Introduction to ArchUnit | Baeldung

« 上一篇: Java周报, 348