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


目錄

  1. Phase 0:準備工作
  2. Phase 1:基礎設定
  3. Phase 2:掃描現有架構
  4. Phase 3:寫第一批規則
  5. Phase 4:逐步擴展
  6. Phase 5:CI 整合
  7. Phase 6:維護與演進
  8. AI 執行清單
  9. 程式碼範例
  10. 附錄:工具比較與參考資料

Phase 0:準備工作

0.1 確認專案基本條件

在導入 ArchUnit 前,先確認以下條件:

條件 檢查方式
Java 8 以上 檢查 pom.xmlbuild.gradlesourceCompatibility
單元測試框架已就緒 JUnit 4 / JUnit 5 / JUnit 6 已加入依賴
編譯後的 .class 檔案可取得 確認 target/classesbuild/classes 存在
建構工具可正常執行測試 mvn testgradle 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(根目錄)的 allprojectssubprojects 中統一管理版本:

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)— 價值最高、風險最低:

  1. 禁止 System.out / System.err 存取
  2. 禁止使用 java.util.logging
  3. Controller 不能直接依賴 Repository
  4. Service 不能依賴 Controller

第二批規則(Week 1)— 進階架構保護:

  1. 定義分層架構並驗證依賴方向
  2. 循環依賴檢測
  3. 命名規範檢查

第三批規則(Month 1)— 精細化控制:

  1. Service 必須以 Service 結尾
  2. Controller 必須以 Controller 結尾
  3. Repository 必須以 Repository 結尾
  4. 禁止使用 Joda-Time(應使用 java.time
  5. 禁止 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 規則的治理流程

  1. 提案:任何人可提議新規則(在 Code Review 或 Architecture Review 中)
  2. 評估:評估對現有程式碼的影響(跑一次看看有多少違規)
  3. 決策:決定是立即執行、用 FreezingArchRule 逐步修復、還是作為低優先級建議
  4. 實作:加入規則並提交
  5. 文件化:在規則的 .as().because() 中說明原因

6.2 規則演進策略

新增專案 → 直接套用完整規則集
既有專案 → FreezingArchRule baseline → 逐步減少違規 → 最終移除 FreezingArchRule

6.3 版本升級

ArchUnit 的版本更新通常是向後相容的。升級時:

<!-- 更新版本 -->
<archunit.version>1.5.0</archunit.version>

升級後執行完整的測試套件確認沒有問題。

6.4 性能考量

  • ArchUnit 分析的是已編譯的 .class 檔案,不是原始碼
  • @AnalyzeClassescacheMode 可以控制快取行為:
  • 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
大量違規導致首次測試失敗 使用 FreezingArchRulearchunit_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 使用。