ArchUnit Java 架構測試研究報告
研究日期:2026-08-23 ArchUnit 最新版本:v1.4.2 (2026-04-18)
1. ArchUnit 是什麼?
ArchUnit 是一個**免費、簡單且可擴展的 Java 架構測試函式庫**,可以用任何純 Java 單元測試框架(JUnit 4 / 5 / 6)來驗證 Java 程式碼的架構規則。它透過分析 Java bytecode,將所有類別匯入一個 Java 程式碼結構中,然後對套件之間的依賴、類別之間的依賴、分層架構、循環依賴等進行檢查。
核心理念:把架構規則寫成自動化的單元測試,讓架構違規在 CI pipeline 中直接失敗,取代人工 Code Review 中的主觀判斷。
| 特性 | 說明 |
|---|---|
| 分析方式 | 讀取 compiled Java bytecode |
| 測試框架 | JUnit 4 / 5 / 6,或任何可執行 Java 的框架 |
| 擴展性 | 完全可自訂的 fluent API |
| 內建規則 | Library API 提供分層架構、洋蔥架構、循環檢測等預設規則 |
來源: - 官方網站:https://www.archunit.org/ - 官方 User Guide:https://www.archunit.org/userguide/html/000_Index.html - GitHub 儲存庫:https://github.com/TNG/ArchUnit - 範例專案:https://github.com/TNG/ArchUnit-Examples
2. 專案設定
Maven 依賴
<!-- JUnit 5 -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>
Gradle 依賴
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
}
基本測試結構
@AnalyzeClasses(
packages = "com.example.myapp",
importOptions = ImportOption.DoNotIncludeTests.class
)
public class ArchitectureTest {
@ArchTest
static final ArchRule myRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
}
使用
ImportOption.DoNotIncludeTests.class排除測試類別,避免誤報。
來源:https://www.archunit.org/userguide/html/000_Index.html
3. 常見檢核類別與規則範例
3.1 分層架構(Layered Architecture)
目的:確保分層之間的單向依賴,防止上層直接依賴下層之外的模組。
方法一:手動定義分層規則
@ArchTest
static final ArchRule layer_dependencies = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
方法二:用類別命名慣例區分分層(適合 package-by-feature 專案)
@ArchTest
static final ArchRule services_should_not_depend_on_controllers = noClasses()
.that().haveSimpleNameEndingWith("Service")
.should().dependOnClassesThat().haveSimpleNameEndingWith("Controller");
@ArchTest
static final ArchRule controllers_should_not_depend_on_repositories = noClasses()
.that().haveSimpleNameEndingWith("Controller")
.should().dependOnClassesThat().haveSimpleNameEndingWith("Repository");
來源: - https://www.archunit.org/userguide/html/000_Index.html#_layer_checks - https://loiane.com/2026/07/architecture-testing-java-archunit
3.2 套件依賴檢查(Package Dependency Checks)
目的:控制哪些套件可以依賴哪些套件。
// source 套件不應依賴 foo 套件
noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..");
// foo 套件只應被特定套件依賴
classes().that().resideInAPackage("..foo..")
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..");
// Service 和 Repository 不應依賴 Controller
noClasses()
.that().resideInAnyPackage("..service..")
.or().resideInAnyPackage("..repository..")
.should().dependOnClassesThat().resideInAnyPackage("..controller..")
.because("Services and repositories should not depend on web layer");
來源: - https://www.archunit.org/userguide/html/000_Index.html#_package_dependency_checks - https://blog.boottechsolutions.com/2024/12/02/spring-boot-3-unit-testing-project-architecture-with-archunit
3.3 命名規範檢查(Naming Convention Checks)
目的:強制類別命名遵循專案慣例。
// 所有 Service 類別名稱必須以 "Service" 結尾,且位於 service 套件
classes().that().haveSimpleNameEndingWith("Service")
.should().resideInAPackage("..service..");
// 所有 Repository 類別必須在 repository 套件
classes().that().haveNameMatching(".*Repository")
.should().resideInAPackage("..repository..")
.as("Repositories should reside in a package '..repository..'");
// 實作 Connection 介面的類別名稱必須以 "Connection" 結尾
classes().that().implement(Connection.class)
.should().haveSimpleNameEndingWith("Connection");
// 公開 API 方法必須使用 camelCase 命名
methods().that().areDeclaredInClassesThat().resideInAPackage("..api..")
.and().arePublic()
.should().haveNameMatching("^[a-z][a-zA-Z0-9]*$");
來源: - https://www.archunit.org/userguide/html/000_Index.html#_inheritance_checks - https://www.bomberbot.com/java/how-to-test-your-java-projects-architecture-with-archunit/
3.4 循環依賴檢查(Cycle Detection)
目的:防止模組之間形成循環依賴,確保架構可維護。
// 使用 slices 檢查套件之間的循環依賴
slices().matching("com.myapp.(*)..").should().beFreeOfCycles();
// 更精細的控制:檢查所有 myapp 下的子套件
slices().matching("..myapp.(**)").should().beFreeOfCycles();
// 確保特定切片之間無循環
slices().matching("..myapp.(**).service..").should().notDependOnEachOther();
進階:自訂切片指派
SliceAssignment customAssignment = new SliceAssignment() {
@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
if (javaClass.getPackageName().startsWith("com.oldapp")) {
return SliceIdentifier.of("Legacy");
}
if (javaClass.getName().contains(".esb.")) {
return SliceIdentifier.of("ESB");
}
return SliceIdentifier.ignore();
}
@Override
public String getDescription() {
return "custom package structure";
}
};
slices().assignedFrom(customAssignment).should().beFreeOfCycles();
來源: - https://www.archunit.org/userguide/html/000_Index.html#_cycle_checks - https://www.adevwrites.space/java/07-testing-quality/archunit
3.5 洋蔥架構 / 六角架構(Onion / Hexagonal Architecture)
目的:驗證洋蔥架構的依賴方向——領域模型為核心,適配器在外圍。
onionArchitecture()
.domainModels("com.myapp.domain.model..")
.domainServices("com.myapp.domain.service..")
.applicationServices("com.myapp.application..")
.adapter("cli", "com.myapp.adapter.cli..")
.adapter("persistence", "com.myapp.adapter.persistence..")
.adapter("rest", "com.myapp.adapter.rest..");
語義規則:
- domainModels 包含領域實體
- domainServices 包含使用實體的領域服務
- applicationServices 包含應用層服務,可使用 domain 項目,但 domain 不可依賴 application
- adapter 包含連接外部系統的邏輯,適配器之間不可互相依賴
來源: - https://www.archunit.org/userguide/html/000_Index.html#_onion_architecture - https://www.codecentric.de/en/knowledge-hub/blog/archunit-in-practice-keep-your-architecture-clean
3.6 類別包含檢查(Containment Checks)
目的:確保特定類別位於正確的套件中。
// 以 "Foo" 開頭的類別必須在 com.foo 套件下
classes().that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo");
// 帶有 @Entity 註解的類別必須在 domain 套件下
classes().that().areAnnotatedWith(Entity.class)
.should().resideInAPackage("..domain..");
來源:https://www.archunit.org/userguide/html/000_Index.html#_class_and_package_containment_checks
3.7 繼承檢查(Inheritance Checks)
目的:確保繼承結構符合架構規範。
// 繼承特定基底類別的類別,名稱必須以特定字串結尾
classes().that().areAssignableTo(AbstractRepository.class)
.should().haveSimpleNameEndingWith("Repository");
// 實作 EntityManager 的類別只能被 persistence 套件的類別依賴
classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..persistence..");
來源:https://www.archunit.org/userguide/html/000_Index.html#_inheritance_checks
3.8 註解檢查(Annotation Checks)
目的:確保特定註解的使用符合規範。
// 帶有 @Transactional 註解的類別,其呼叫者也必須有 @Transactional\n
classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat().areAnnotatedWith(Transactional.class);
// Controller 的所有公開方法必須帶有 @RequestMapping 或 @GetMapping 等註解
methods().that().areDeclaredInClassesThat()
.haveSimpleNameEndingWith("Controller")
.and().arePublic()
.should().beAnnotatedWith(Mapping.class)
.because("All public controller methods should be mapped to an endpoint");
來源:https://www.archunit.org/userguide/html/000_Index.html#_annotation_checks
3.9 一般編碼規則(General Coding Rules)
ArchUnit 內建了一組通用的編碼規則,可直接使用。
禁止擲出泛型例外
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
@ArchTest
static final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
禁止使用 System.out / System.err
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
@ArchTest
static final ArchRule no_standard_streams = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
禁止使用 JodaTime(應使用 java.time)
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JODATIME;
@ArchTest
static final ArchRule no_jodatime = NO_CLASSES_SHOULD_USE_JODATIME;
禁止使用 java.util.logging
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;
@ArchTest
static final ArchRule no_jul = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;
禁止使用欄位注入(應使用建構函式注入)
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION;
@ArchTest
static final ArchRule no_field_injection = NO_CLASSES_SHOULD_USE_FIELD_INJECTION;
完整常數一覽(來自 GeneralCodingRules):
| 常數 | 說明 |
|---|---|
NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS |
禁止擲出 Exception / RuntimeException / Throwable |
NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS |
禁止使用 System.out / System.err |
NO_CLASSES_SHOULD_USE_JODATIME |
禁止使用 JodaTime |
NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING |
禁止使用 java.util.logging |
NO_CLASSES_SHOULD_USE_FIELD_INJECTION |
禁止使用 @Autowired / @Inject / @Resource 欄位注入 |
來源: - https://github.com/TNG/ArchUnit/blob/main/archunit/src/main/java/com/tngtech/archunit/library/GeneralCodingRules.java - https://blog.scottlogic.com/2019/12/05/unit-test-your-architecture-with-archunit.html
3.10 模組化規則(Modularization Rules)
目的:檢查模組之間的依賴關係是否符合規範。
modules().that().resideInAPackage("..domain..")
.should().notDependOnModulesThat().resideInAPackage("..infrastructure..");
modules().definedByPackages("..myapp.(*)..")
.should().notDependOnEachOther();
來源:https://www.archunit.org/userguide/html/000_Index.html#_modularization_rules
3.11 公開 API 可見性檢查
目的:限制公開 API 的範圍。
// 只有 api 套件中的類別可以是 public
classes().that().arePublic()
.should().resideInAPackage("..api..");
// 公開方法不應存取標準串流
methods().that().arePublic()
.should().notCallMethod(System.class, "out")
.because("Production code should use a logging framework");
來源:https://www.archunit.org/userguide/html/000_Index.html#_the_lang_api
3.12 架構度量(Architecture Metrics)
ArchUnit 也支援軟體架構度量的計算:
- CCD(Cumulative Component Dependency):所有元件 DependsOn 值的總和
- ACD(Average Component Dependency):CCD 除以元件數量
- RACD(Relative ACD):ACD 除以元件數量
- NCCD(Normalized CCD):系統 CCD 除以平衡二元樹 CCD
import com.tngtech.archunit.library.metrics.ArchitectureMetrics;
JavaClasses classes = // ...
Set<JavaPackage> packages = classes.getPackage("com.example").getSubpackages();
MetricsComponents<JavaClass> components = MetricsComponents.fromPackages(packages);
LakosMetrics metrics = ArchitectureMetrics.lakosMetrics(components);
System.out.println("CCD: " + metrics.getCumulativeComponentDependency());
System.out.println("ACD: " + metrics.getAverageComponentDependency());
來源:https://www.archunit.org/userguide/html/000_Index.html#_software_architecture_metrics
4. 業界最佳實踐
4.1 選擇適合的規則粒度
- 從分層架構開始:先定義 Controller → Service → Repository 的依賴方向
- 再加入命名規範:確保
*Service、*Repository、*Controller的命名一致性 - 最後加入循環檢測:防止模組之間形成循環
4.2 排除測試類別
始終使用 ImportOption.DoNotIncludeTests.class,避免測試輔助類別造成誤報:
@AnalyzeClasses(
packages = "com.example",
importOptions = ImportOption.DoNotIncludeTests.class
)
4.3 使用 archunit_ignore_patterns.txt 處理遺留程式碼
在專案根目錄建立 archunit_ignore_patterns.txt,忽略已知的違規:
# 忽略 LegacyService 的所有違規
.*some\.pkg\.LegacyService.*
來源:https://www.archunit.org/userguide/html/000_Index.html#_ignoring_violations
4.4 使用 because() 說明規則原因
noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..")
.because("Service layer should be independent of the web layer");
4.5 組合多條規則
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
// 組合規則
static final ArchRule general_coding_rules = CompositeArchRule
.of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS)
.and(NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS);
4.6 使用 Taikai 預設規則
Taikai 函式庫擴展了 ArchUnit,提供針對 Spring Boot、Jakarta EE 等框架的預設規則:
ArchRule rule = ArchRuleDefinition.noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
來源:https://github.com/enofex/taikai
4.7 在 CI/CD 中執行
將 ArchUnit 測試納入 CI pipeline,確保每次提交都通過架構檢查:
# GitHub Actions 範例
- name: Run Architecture Tests
run: mvn test -Dtest=ArchitectureTest
4.8 循序漸進地引入
對於遺留專案,建議:
1. 先用 archunit_ignore_patterns.txt 忽略所有現有違規
2. 逐步減少 ignore patterns
3. 確保沒有新的違規被引入
4.9 文件化架構決策
將 ArchUnit 規則與 ADR(Architecture Decision Records)結合,記錄每條規則存在的原因。
來源: - https://www.codecentric.de/en/knowledge-hub/blog/archunit-in-practice-keep-your-architecture-clean - https://christopher-bimson.github.io/2024/07/enforcing-architecture-constraints-with-archunit/ - https://www.freecodecamp.org/news/java-archunit-testing-the-architecture-a09f089585be
5. 參考資源
6. 總結
ArchUnit 將架構規則轉化為可執行的單元測試,是現代 Java 專案維護架構完整性的利器。常見的檢核類別包括:
- 分層架構:確保 Controller → Service → Repository 的單向依賴
- 套件依賴:控制哪些套件可以依賴哪些套件
- 命名規範:強制
*Service、*Repository、*Controller等命名慣例 - 循環依賴:防止模組之間形成循環
- 洋蔥 / 六角架構:驗證領域模型為核心的依賴方向
- 繼承 / 註解檢查:確保繼承結構和註解使用符合規範
- 一般編碼規則:禁止泛型例外、System.out、JodaTime、欄位注入等反模式
- 架構度量:計算 CCD、ACD、RACD 等複雜度指標
建議從分層架構和命名規範開始,循序漸進地加入更多規則,並在 CI/CD 中自動化執行。