ArchUnit 專案導入實戰手冊
用途:讓 AI 能快速協助任何 Java 專案導入 ArchUnit,逐步建立架構自動化測試。
適用對象:已有 Java 程式碼庫,希望開始自動化驗證架構規則的團隊。
最新版本:ArchUnit 1.5.0(2026-08 發布)
官方資源: - 官網:https://www.archunit.org - 使用手冊:https://www.archunit.org/userguide/html/000_Index.html - 範例程式碼:https://github.com/TNG/ArchUnit-Examples
目錄
- Phase 0:準備工作
- Phase 1:基礎設定
- Phase 2:掃描現有架構
- Phase 3:寫第一批規則
- Phase 4:逐步擴展
- Phase 5:CI 整合
- Phase 6:維護與演進
- AI 執行清單
- 程式碼範例
- 附錄:工具比較與參考資料
Phase 0:準備工作
0.1 確認專案基本條件
在導入 ArchUnit 前,先確認以下條件:
| 條件 | 檢查方式 |
|---|---|
| Java 8 以上 | 檢查 pom.xml 或 build.gradle 的 sourceCompatibility |
| 單元測試框架已就緒 | JUnit 4 / JUnit 5 / JUnit 6 已加入依賴 |
編譯後的 .class 檔案可取得 |
確認 target/classes 或 build/classes 存在 |
| 建構工具可正常執行測試 | mvn test 或 gradle test 可通過 |
0.2 分析專案結構
使用以下指令來理解專案結構:
# Maven 專案
find src/main/java -type d | head -30
# Gradle 專案
find src/main/java -type d | head -30
# 確認套件結構
ls -R src/main/java/
需要掌握的資訊:
- 根套件名稱(例如
com.mycompany.myapp) - 架構分層方式(例如 controller / service / repository / model)
- 模組數量(單模組 vs 多模組 Maven/Gradle 專案)
- 是否有共同模組(shared/common module)
- 外部框架(Spring Boot / Jakarta EE / 純 Java)
0.3 識別現有架構問題
在寫規則之前,先觀察是否有以下常見問題(這些日後會成為第一批規則):
# 檢查是否有 Controller 直接依賴 Repository
grep -r "Repository" src/main/java/**/controller/
# 檢查是否有循環依賴(可用 jdeps 或 IDE 功能)
jdeps -s target/classes
# 檢查是否有不當的全域靜態存取
grep -r "System.out\|System.err" src/main/java/
Phase 1:基礎設定
1.1 加入 Maven 依賴
根據你的 JUnit 版本,選擇對應的 artifact:
JUnit 5(最常見)
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
JUnit 6(最新)
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit6</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
JUnit 4
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
1.2 加入 Gradle 依賴
JUnit 5
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.5.0'
}
JUnit 6
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit6:1.5.0'
}
JUnit 4
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit4:1.5.0'
}
多模組 Gradle 專案
在 build.gradle(根目錄)的 allprojects 或 subprojects 中統一管理版本:
subprojects {
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.5.0'
}
}
1.3 版本管理建議
在 Maven pom.xml 的 <properties> 中統一管理版本:
<properties>
<archunit.version>1.5.0</archunit.version>
</properties>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
在 Gradle 中使用 Version Catalog(gradle/libs.versions.toml):
[versions]
archunit = "1.5.0"
[libraries]
archunit-junit5 = { group = "com.tngtech.archunit", name = "archunit-junit5", version.ref = "archunit" }
1.4 確認 Maven Surefire Plugin 版本
ArchUnit JUnit ⅚ 需要 Surefire 2.22.0 以上。Spring Boot 專案通常已內建,但建議明確指定:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
注意:ArchUnit 使用自己的
ArchUnitTestEngine,不是 JUnit Jupiter 的 engine。如果使用@Tag過濾測試,需要改用@ArchTag。
1.5 Gradle 測試配置
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
1.6 建立第一個 ArchUnit 測試類別
src/test/java/
└── com/mycompany/myapp/
└── architecture/
└── ArchitectureTest.java
Phase 2:掃描現有架構
2.1 使用 ClassFileImporter 探索程式碼
在正式寫規則之前,先用 ArchUnit 的 Core API 探索程式碼結構:
@Test
void explore_architecture() {
JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.mycompany.myapp");
// 列出所有套件
classes.forEach(c -> System.out.println(c.getPackageName() + "." + c.getSimpleName()));
// 統計各套件的類別數量
Map<String, Long> packageStats = classes.stream()
.collect(Collectors.groupingBy(JavaClass::getPackageName, Collectors.counting()));
packageStats.forEach((pkg, count) -> System.out.println(pkg + ": " + count));
}
2.2 分析依賴關係
找出哪些套件互相依賴:
@Test
void analyze_dependencies() {
JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.mycompany.myapp");
// 列出所有套件間的依賴
classes.forEach(clazz -> {
clazz.getAccessesFromSelf().forEach(access -> {
String targetPkg = access.getTargetOwner().getPackageName();
if (!clazz.getPackageName().equals(targetPkg)) {
System.out.println(clazz.getName() + " -> " + access.getTargetOwner().getName());
}
});
});
}
2.3 識別應當遵守的架構規則
根據掃描結果,判斷哪些規則最值得先寫:
| 優先級 | 規則類型 | 價值 |
|---|---|---|
| P0 | 防止循環依賴 | 避免修改困難、編譯速度下降 |
| P1 | 分層架構依賴方向 | 維護關注點分離 |
| P2 | 命名規範 | 一致性、可讀性 |
| P3 | 禁止不當使用(如 System.out) |
代碼品質 |
| P4 | 標註規範(如 Service 必須加 @Service) |
Spring 最佳實踐 |
2.4 確認現有違規數量
先跑一次看看有多少違規,決定是否需要使用 FreezingArchRule:
@Test
void count_existing_violations() {
JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.mycompany.myapp");
ArchRule rule = noClasses().that().resideInAPackage("..controller..")
.should().dependOnClassesThat().resideInAPackage("..repository..");
try {
rule.check(classes);
System.out.println("No violations found!");
} catch (AssertionError e) {
System.out.println("Violations: " + e.getMessage());
}
}
Phase 3:寫第一批規則
3.1 規則優先順序建議
第一批規則(Day 1)— 價值最高、風險最低:
- 禁止
System.out/System.err存取 - 禁止使用
java.util.logging - Controller 不能直接依賴 Repository
- Service 不能依賴 Controller
第二批規則(Week 1)— 進階架構保護:
- 定義分層架構並驗證依賴方向
- 循環依賴檢測
- 命名規範檢查
第三批規則(Month 1)— 精細化控制:
- Service 必須以
Service結尾 - Controller 必須以
Controller結尾 - Repository 必須以
Repository結尾 - 禁止使用 Joda-Time(應使用
java.time) - 禁止 field injection(應使用建構子注入)
3.2 建立規則類別結構
建議的目錄結構:
src/test/java/
└── com/mycompany/myapp/
└── architecture/
├── ArchitectureTest.java # 主測試類別(@AnalyzeClasses)
├── CodingRulesTest.java # 編碼規則
├── LayerDependencyTest.java # 分層依賴規則
├── NamingRulesTest.java # 命名規範
├── NamingConventionTest.java # 命名慣例
└── rules/
└── CustomRules.java # 可複用的自訂規則
3.3 第一批規則完整範例
見 程式碼範例 章節。
Phase 4:逐步擴展
4.1 使用 FreezingArchRule 處理既有違規
當現有程式碼已有大量違規時,FreezingArchRule 是關鍵工具:
原理:
- 第一次執行時,所有違規會被記錄到 ViolationStore(預設是純文字檔)
- 後續執行只會報告**新增**的違規
- 當違規被修復時,FreezingArchRule 會自動減少已記錄的違規
設定步驟:
步驟 1:建立 archunit.properties
在 src/test/resources/archunit.properties:
# 允許建立新的 Violation Store(僅在第一次執行時需要)
freeze.store.default.allowStoreCreation=true
# Violation Store 的路徑(建議放在 VCS 中追蹤)
freeze.store.default.path=src/test/resources/frozen
# 顯示名稱將底線取代為空格(較易讀)
junit.displayName.replaceUnderscoresBySpaces=true
步驟 2:使用 FreezingArchRule
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class ArchitectureTest {
@ArchTest
static final ArchRule services_should_not_depend_on_controllers =
FreezingArchRule.freeze(
classes().that().resideInAPackage("..service..")
.should().notDependOnClassesThat().resideInAPackage("..controller..")
);
}
步驟 3:首次執行,建立 baseline
執行一次測試,此時所有已知違規會被記錄:
mvn test -Darchunit.freeze.store.default.allowStoreCreation=true
步驟 4:提交 Violation Store 到版本控制
git add src/test/resources/frozen/
git commit -m "建立 ArchUnit baseline:記錄已知違規"
步驟 5:CI 環境設定
在 CI 中禁用 store 的建立和更新:
mvn test \
-Darchunit.freeze.store.default.allowStoreCreation=false \
-Darchunit.freeze.store.default.allowStoreUpdate=false
4.2 使用 archunit_ignore_patterns.txt 忽略特定違規
針對特定 Legacy 類別或模組,可以用正則表達式忽略違規:
在 src/test/resources/archunit_ignore_patterns.txt:
# 忽略舊模組的已知違規
.*com\.mycompany\.legacy\..*
# 忽略特定類別
.*LegacyService.*
.*OldRepository.*
4.3 使用 Priority 分級規則
// 高優先級:破壞就應該失敗
static final ArchRule no_cycles = ArchRuleDefinition.priority(Priority.HIGH)
.slices().matching("com.mycompany.myapp.(*)..")
.should().beFreeOfCycles();
// 低優先級:作為建議,不阻擋建構
static final ArchRule naming_convention = ArchRuleDefinition.priority(Priority.LOW)
.classes().that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service");
4.4 建立可複用的規則庫
將通用規則抽取到共用模組:
// 共用規則模組中的規則定義
public final class ArchUnitRules {
public static final ArchRule NO_FIELD_INJECTION = classes()
.that().resideInAPackage("..service..")
.should().accessClassesThat()
.resideInAPackage("..controller..")
.as("Service 不應依賴 Controller");
public static final ArchRule NO_SYSTEM_OUT = GeneralCodingRules
.noClassesShouldAccessStandardStreams();
public static final ArchRule NO_JODATIME = GeneralCodingRules
.noClassesShouldUseJodaTime();
public static final ArchRule NO_LOGGING_UTIL = GeneralCodingRules
.noClassesShouldUseJavaUtilLogging();
}
在各專案的測試中引用:
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class ArchitectureTest {
@ArchTest
static final ArchRule no_field_injection = ArchUnitRules.NO_FIELD_INJECTION;
@ArchTest
static final ArchRule no_system_out = ArchUnitRules.NO_SYSTEM_OUT;
}
4.5 使用 Library API 的內建規則
分層架構
layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.layer("Domain").definedBy("..domain..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Service", "Persistence");
六角架構(Onion / Hexagonal)
onionArchitecture()
.domainModels("com.mycompany.app.domain.model..")
.domainServices("com.mycompany.app.domain.service..")
.applicationServices("com.mycompany.app.application..")
.adapter("rest", "com.mycompany.app.adapter.rest..")
.adapter("persistence", "com.mycompany.app.adapter.persistence..")
.adapter("messaging", "com.mycompany.app.adapter.messaging..");
循環依賴檢測
slices().matching("com.mycompany.myapp.(*)..")
.should().beFreeOfCycles();
模組化規則
modules().definedByPackages("com.mycompany.myapp.(*)..")
.should().beFreeOfCycles();
Phase 5:CI 整合
5.1 Maven CI 整合
ArchUnit 測試就是一般的 JUnit 測試,所以 mvn test 就會自動執行。
# GitHub Actions 範例
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build and Test
run: mvn verify
5.2 Gradle CI 整合
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build and Test
run: ./gradlew build
5.3 使用 Société Générale Maven Plugin(選擇性)
如果不想用 JUnit 方式執行,可以用 Maven Plugin 直接在建構生命週期中執行 ArchUnit:
<plugin>
<groupId>com.societe-generale</groupId>
<artifactId>arch-unit-maven-plugin</artifactId>
<version>1.1.1</version>
<configuration>
<failOnViolation>true</failOnViolation>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
5.4 ArchUnit 的 @ArchTag 替代 JUnit 5 的 @Tag
重要:ArchUnit 使用自己的 TestEngine(ArchUnitTestEngine),不支援 JUnit Jupiter 的 @Tag。要標記 ArchUnit 測試,請使用 @ArchTag:
@ArchTag("architecture")
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class ArchitectureTest {
// ...
}
在 Gradle 中過濾 ArchUnit 測試:
test {
useJUnitPlatform {
// ArchUnit 的 tag 使用 @ArchTag,不是 @Tag
includeTags 'architecture'
}
}
5.5 測試報告
ArchUnit 的錯誤訊息已經很詳細,包含違規的類別名、方法名和行號。若需要更豐富的報告,可自訂 FailureDisplayFormat:
public class DetailedFailureFormat implements FailureDisplayFormat {
@Override
public String formatFailure(HasDescription rule, FailureMessages failureMessages, Priority priority) {
String details = failureMessages.stream()
.map(msg -> " - " + msg)
.collect(Collectors.joining("\n"));
return String.format(
"架構違規 [優先級: %s]\n規則: %s\n違規數: %s\n%s",
priority.asString(), rule.getDescription(),
failureMessages.getInformationAboutNumberOfViolations(), details
);
}
}
在 archunit.properties 中設定:
failureDisplayFormat=com.mycompany.architecture.DetailedFailureFormat
Phase 6:維護與演進
6.1 規則的治理流程
- 提案:任何人可提議新規則(在 Code Review 或 Architecture Review 中)
- 評估:評估對現有程式碼的影響(跑一次看看有多少違規)
- 決策:決定是立即執行、用 FreezingArchRule 逐步修復、還是作為低優先級建議
- 實作:加入規則並提交
- 文件化:在規則的
.as()或.because()中說明原因
6.2 規則演進策略
新增專案 → 直接套用完整規則集
既有專案 → FreezingArchRule baseline → 逐步減少違規 → 最終移除 FreezingArchRule
6.3 版本升級
ArchUnit 的版本更新通常是向後相容的。升級時:
<!-- 更新版本 -->
<archunit.version>1.5.0</archunit.version>
升級後執行完整的測試套件確認沒有問題。
6.4 性能考量
- ArchUnit 分析的是已編譯的
.class檔案,不是原始碼 @AnalyzeClasses的cacheMode可以控制快取行為:PER_CLASS:每個測試類別各自快取(預設)PER_METHOD:每個測試方法各自快取(最慢但最安全)- 對於大型專案,建議將 ArchUnit 測試放在單獨的 Maven profile 中,避免影響開發時的測試速度
6.5 常見問題排解
| 問題 | 解決方案 |
|---|---|
| ArchUnit 測試未被 Maven 執行 | 確認 Surefire 版本 ≥ 2.22.0;不要在 Surefire 的 groups 配置中使用 JUnit 5 的 @Tag,改用 @ArchTag |
| Gradle 未找到 ArchUnit 測試 | 確認 useJUnitPlatform() 已設定;使用 @ArchTag 而非 @Tag |
Kotlin 延伸函式產生的 $Kt 類別導致命名規範違規 |
使用 .areTopLevelClasses() 過濾,或將延伸函式放入 companion object |
| FreezingArchRule 在 CI 中嘗試建立 store | 在 CI 設定 -Darchunit.freeze.store.default.allowStoreCreation=false |
| 報告中 classname 缺少套件名稱 | 這是 Surefire + JUnit 5 的已知問題,可在 Surefire 中設定 statelessTestsetReporter |
| 大量違規導致首次測試失敗 | 使用 FreezingArchRule 或 archunit_ignore_patterns.txt |
AI 執行清單
以下清單供 AI 依序執行,協助專案完成 ArchUnit 導入:
Step 1:掃描專案結構
[ ] 確認 Java 版本(pom.xml 或 build.gradle)
[ ] 確認建構工具(Maven / Gradle)
[ ] 確認測試框架版本(JUnit 4 / 5 / 6)
[ ] 識別根套件名稱
[ ] 繪製套件結構圖
[ ] 確認是否有共同模組
[ ] 執行 mvn dependency:tree 或 gradle dependencies 確認無衝突
Step 2:加入依賴
[ ] 選擇對應 JUnit 版本的 artifact
[ ] 在 pom.xml 或 build.gradle 中加入依賴
[ ] 確認 Maven Surefire Plugin 版本 ≥ 2.22.0
[ ] 確認 Gradle 有 useJUnitPlatform() 設定
[ ] 執行 mvn test 或 gradle test 確認建構正常
Step 3:建立第一批規則
[ ] 建立 src/test/java/.../architecture/ 目錄
[ ] 建立 ArchitectureTest.java,設定 @AnalyzeClasses
[ ] 加入第一條規則:禁止 System.out(使用 GeneralCodingRules)
[ ] 加入第二條規則:禁止 java.util.logging
[ ] 加入第三條規則:Controller 不依賴 Repository
[ ] 加入第四條規則:Service 不依賴 Controller
[ ] 執行測試確認結果
[ ] 根據違規數量決定是否使用 FreezingArchRule
Step 4:建立 FreezingArchRule(如需要)
[ ] 建立 src/test/resources/archunit.properties
[ ] 設定 freeze.store.default.path
[ ] 設定 freeze.store.default.allowStoreCreation=true
[ ] 用 FreezingArchRule.freeze() 包裝有大量違規的規則
[ ] 首次執行建立 baseline
[ ] 提交 frozen/ 目錄到版本控制
[ ] 設定 CI 為 allowStoreCreation=false, allowStoreUpdate=false
Step 5:擴展規則集
[ ] 加入分層架構規則(layeredArchitecture 或 onionArchitecture)
[ ] 加入循環依賴檢測
[ ] 加入命名規範規則
[ ] 加入 @ArchTag 標記(替代 @Tag)
[ ] 建立可複用的規則常數類別
[ ] 為重要規則加上 .because() 說明原因
Step 6:CI 整合
[ ] 在 CI pipeline 中確認 mvn test / gradle test 會執行 ArchUnit
[ ] 設定 CI 環境的 archunit.properties 系統屬性
[ ] 確認失敗的 ArchUnit 測試會阻擋合併
[ ] 考慮加入測試報告上傳(如有需要)
Step 7:文件化與交接
[ ] 在 README 或 Architecture Decision Record 中記錄架構規則
[ ] 說明每條規則的原因(.because())
[ ] 建立新人指引:如何新增規則
[ ] 定期(每季度)審視規則是否仍然適用
程式碼範例
範例 1:基礎架構測試類別
package com.mycompany.myapp.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchitectureTest {
private final JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.mycompany.myapp");
@Test
void services_should_not_depend_on_controllers() {
ArchRule rule = noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
rule.check(classes);
}
}
範例 2:使用 @AnalyzeClasses(JUnit 5 整合)
package com.mycompany.myapp.architecture;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.GeneralCodingRules.*;
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class CodingRulesTest {
@ArchTest
static final ArchRule no_system_out = noClasses()
.should().accessClassesThat()
.haveSimpleName("System");
@ArchTest
static final ArchRule no_jodatime = noClasses()
.should().dependOnClassesThat()
.resideInAPackage("..joda..");
@ArchTest
static final ArchRule no_java_util_logging = noClasses()
.should().dependOnClassesThat()
.resideInAPackage("..java.util.logging..");
}
範例 3:分層架構規則
package com.mycompany.myapp.architecture;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class LayerDependencyTest {
@ArchTest
static final ArchRule layered_architecture = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.layer("Domain").definedBy("..domain..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
.whereLayer("Domain").mayNotAccessAnyLayer();
}
範例 4:六角架構規則
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class HexagonalArchitectureTest {
@ArchTest
static final ArchRule hexagonal_architecture = onionArchitecture()
.domainModels("com.mycompany.myapp.domain.model..")
.domainServices("com.mycompany.myapp.domain.service..")
.applicationServices("com.mycompany.myapp.application..")
.adapter("rest", "com.mycompany.myapp.adapter.rest..")
.adapter("persistence", "com.mycompany.myapp.adapter.persistence..")
.adapter("messaging", "com.mycompany.myapp.adapter.messaging..");
}
範例 5:命名規範規則
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class NamingRulesTest {
@ArchTest
static final ArchRule controllers_must_end_with_controller = classes()
.that().resideInAPackage("..controller..")
.should().haveSimpleNameEndingWith("Controller");
@ArchTest
static final ArchRule services_must_end_with_service = classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service");
@ArchTest
static final ArchRule repositories_must_end_with_repository = classes()
.that().resideInAPackage("..repository..")
.should().haveSimpleNameEndingWith("Repository");
@ArchTest
static final ArchRule entities_must_end_with_entity = classes()
.that().resideInAPackage("..domain.model..")
.and().areTopLevelClasses()
.should().haveSimpleNameEndingWith("Entity");
}
範例 6:FreezingArchRule 完整範例
package com.mycompany.myapp.architecture;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.library.freeze.FreezingArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class LegacyArchitectureTest {
// 有大量既有違規的規則,使用 FreezingArchRule 逐步修復
@ArchTest
static final ArchRule no_circular_dependencies = FreezingArchRule.freeze(
slices().matching("com.mycompany.myapp.(*)..")
.should().beFreeOfCycles()
.as("套件之間不應有循環依賴")
);
// 已知有很多違規的分層規則
@ArchTest
static final ArchRule services_should_not_depend_on_controllers = FreezingArchRule.freeze(
noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..")
.as("Service 不應依賴 Controller")
.because("違反關注點分離原則")
);
}
src/test/resources/archunit.properties:
freeze.store.default.path=src/test/resources/frozen
freeze.store.default.allowStoreCreation=true
junit.displayName.replaceUnderscoresBySpaces=true
範例 7:自訂 ArchCondition
package com.mycompany.myapp.architecture.rules;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
public class ShouldNotHaveCircularDependencies extends ArchCondition<JavaClass> {
public ShouldNotHaveCircularDependencies() {
super("不應有循環依賴");
}
@Override
public void check(JavaClass item, ConditionEvents events) {
// 自訂檢查邏輯
for (JavaClass dependency : item.getDirectDependenciesFromSelf()) {
if (dependency.getDirectDependenciesToSelf().contains(item)) {
events.add(SimpleConditionEvent.violated(item,
String.format("%s 與 %s 之間有循環依賴",
item.getName(), dependency.getName())));
}
}
}
}
使用方式:
ArchRule rule = classes()
.that().resideInAPackage("..service..")
.should(new ShouldNotHaveCircularDependencies());
範例 8:自訂 DescribedPredicate
package com.mycompany.myapp.architecture.rules;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
public class CustomPredicates {
// 自訂述詞:檢查是否為 Spring Controller
public static final DescribedPredicate<JavaClass> ARE_SPRING_CONTROLLERS =
new DescribedPredicate<JavaClass>("是 Spring Controller") {
@Override
public boolean test(JavaClass input) {
return input.isAnnotatedWith("org.springframework.web.bind.annotation.RestController")
|| input.isAnnotatedWith("org.springframework.stereotype.Controller");
}
};
// 使用方式
public static void controllers_should_not_depend_on_each_other() {
ArchRuleDefinition.classes()
.that(ARE_SPRING_CONTROLLERS)
.should().notDependOnClassesThat(ARE_SPRING_CONTROLLERS)
.because("Controller 之間不應有直接依賴");
}
}
範例 9:多模組專案的 ArchUnit 設定
在多模組 Maven 專案中,可以在每個子模組中放自己的規則,也可以在整合測試模組中統一檢查:
// 在 integration-test 模組中
@AnalyzeClasses(packages = "com.mycompany")
public class CrossModuleArchitectureTest {
@ArchTest
static final ArchRule module_dependencies = modules()
.definedByPackages("com.mycompany.(*)..")
.should().beFreeOfCycles();
@ArchTest
static final ArchRule api_module_rules = classes()
.that().resideInAPackage("..api..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..api..", "..common..", "java..", "javax..");
}
範例 10:Kotlin 專案的 ArchUnit
package com.mycompany.myapp.architecture
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.core.importer.ImportOption
import com.tngtech.archunit.junit.AnalyzeClasses
import com.tngtech.archunit.junit.ArchTest
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition
@AnalyzeClasses(packagesOf = [ArchitectureTest::class])
class ArchitectureTest {
companion object {
@JvmStatic
val classes = ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.mycompany.myapp")
}
@ArchTest
val no_field_injection = ArchRuleDefinition.noClasses()
.that().resideInAPackage("..service..")
.should().accessClassesThat()
.haveSimpleName("Autowired")
.because("應使用建構子注入,而非欄位注入")
@ArchTest
fun services_should_not_depend_on_controllers(importedClasses: com.tngtech.archunit.core.domain.JavaClasses) {
ArchRuleDefinition.noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..")
.check(importedClasses)
}
}
附錄
A. 工具比較
| 特性 | ArchUnit | JDepend | dependency-cruiser |
|---|---|---|---|
| 語言 | Java / Kotlin | Java | JavaScript / TypeScript |
| 分析方式 | 分析 bytecode | 分析 bytecode | 分析 import/require |
| 與測試框架整合 | JUnit ⅘/6 | 需自行整合 | CLI 工具 |
| CI 整合 | 直接作為測試執行 | 需外掛 | CLI 直接執行 |
| 規則表達力 | 極高(Fluent API) | 中等 | 高(JSON config) |
| 社群活躍度 | 高(TNG 維護) | 低 | 高 |
| 適合場景 | Java 專案 | Java 專案(較舊) | JS/TS 專案 |
| 免費 | 是(Apache 2.0) | 是 | 是(MIT) |
B. ArchUnit 模組一覽
| 模組 | 用途 |
|---|---|
archunit |
核心模組,可搭配任何測試框架 |
archunit-junit4 |
JUnit 4 整合(含 ArchUnitRunner) |
archunit-junit5-api |
JUnit 5 API(編譯時依賴) |
archunit-junit5-engine |
JUnit 5 執行引擎 |
archunit-junit5 |
JUnit 5 便利依賴(包含 api + engine) |
archunit-junit6-api |
JUnit 6 API |
archunit-junit6-engine |
JUnit 6 執行引擎 |
archunit-junit6 |
JUnit 6 便利依賴 |
C. archunit.properties 完整配置參考
# === 基本設定 ===
# 顯示名稱底線取代為空格
junit.displayName.replaceUnderscoresBySpaces=true
# 空的 should 不報錯(預設 true)
archRule.failOnEmptyShould=false
# === 循環依賴偵測 ===
# 偵測的最大循環數(預設 100)
cycles.maxNumberToDetect=50
# 每個循環邊的最大依賴報告數(預設 20)
cycles.maxNumberOfDependenciesPerEdge=5
# === FreezingArchRule ===
# Violation Store 路徑
freeze.store.default.path=src/test/resources/frozen
# 允許建立新 store(預設 false)
freeze.store.default.allowStoreCreation=true
# 允許更新 store(預設 true)
freeze.store.default.allowStoreUpdate=false
# 允許重新凍結所有違規(預設 false)
freeze.refreeze=false
# === 自訂違規顯示格式 ===
failureDisplayFormat=com.mycompany.architecture.DetailedFailureFormat
# === 過濾測試 ===
# 指定要執行的 ArchUnit 規則欄位名稱(逗號分隔)
junit.testFilter=my_custom_rule_field
D. 參考資源
| 資源 | 連結 |
|---|---|
| ArchUnit 官方網站 | https://www.archunit.org |
| 使用手冊 | https://www.archunit.org/userguide/html/000_Index.html |
| GitHub 範例 | https://github.com/TNG/ArchUnit-Examples |
| Maven Central | https://mvnrepository.com/artifact/com.tngtech.archunit/archunit |
| Taikai(預建規則庫) | https://github.com/enofex/taikai |
| Société Générale Maven Plugin | https://github.com/societe-generale/arch-unit-maven-plugin |
| FreezingArchRule 文件 | https://www.archunit.org/userguide/html/000_Index.html#_freezing_arch_rules |
| Baeldung 介紹文章 | https://www.baeldung.com/java-archunit-intro |
| codecentric 實戰文章 | https://www.codecentric.de/en/knowledge-hub/blog/archunit-in-practice-keep-your-architecture-clean |
| Enterprise Adoptio 策略 | https://www.learnteachmaster.org/post/enterprise-archunit |
E. 術語表
| 術語 | 說明 |
|---|---|
| ArchRule | 一條架構規則的定義 |
| @ArchTest | 標記 ArchUnit 測試方法或欄位的註解 |
| @AnalyzeClasses | 指定要分析的套件範圍 |
| ClassFileImporter | 導入 Java bytecode 的核心類別 |
| FreezingArchRule | 凍結現有違規,僅報告新增違規 |
| ViolationStore | 儲存已知違規的機制 |
| archunit.properties | ArchUnit 的全域設定檔 |
| @ArchTag | ArchUnit 專用的標記註解(替代 JUnit 5 的 @Tag) |
| DescribedPredicate | 自訂的類別篩選條件 |
| ArchCondition | 自訂的架構條件檢查 |
| Library API | ArchUnit 內建的高階規則 API |
| GeneralCodingRules | 常見的編碼品質規則集合 |
文件版本:v1.0 | 建立日期:2026-08-23 | ArchUnit 版本:1.5.0
本文件使用繁體中文撰寫,供 AI 快速協助 Java 專案導入 ArchUnit 使用。