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. 參考資源

資源 連結
ArchUnit 官網 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
GeneralCodingRules 原始碼 https://github.com/TNG/ArchUnit/blob/main/archunit/src/main/java/com/tngtech/archunit/library/GeneralCodingRules.java
DeepWiki 使用案例 https://deepwiki.com/TNG/ArchUnit/5.3-examples-and-use-cases
ThoughtWorks Technology Radar https://www.thoughtworks.com/radar/tools/archunit
Taikai 預設規則 https://github.com/enofex/taikai
實戰指南 (codecentric) https://www.codecentric.de/en/knowledge-hub/blog/archunit-in-practice-keep-your-architecture-clean
Loiane 實戰教學 https://loiane.com/2026/07/architecture-testing-java-archunit
FreeCodeCamp 教學 https://www.freecodecamp.org/news/java-archunit-testing-the-architecture-a09f089585be
Scott Logic 教學 https://blog.scottlogic.com/2019/12/05/unit-test-your-architecture-with-archunit.html
MyDeveloperPlanet 教學 https://mydeveloperplanet.com/2025/04/30/enforcing-architecture-with-archunit-in-java/

6. 總結

ArchUnit 將架構規則轉化為可執行的單元測試,是現代 Java 專案維護架構完整性的利器。常見的檢核類別包括:

  1. 分層架構:確保 Controller → Service → Repository 的單向依賴
  2. 套件依賴:控制哪些套件可以依賴哪些套件
  3. 命名規範:強制 *Service*Repository*Controller 等命名慣例
  4. 循環依賴:防止模組之間形成循環
  5. 洋蔥 / 六角架構:驗證領域模型為核心的依賴方向
  6. 繼承 / 註解檢查:確保繼承結構和註解使用符合規範
  7. 一般編碼規則:禁止泛型例外、System.out、JodaTime、欄位注入等反模式
  8. 架構度量:計算 CCD、ACD、RACD 等複雜度指標

建議從分層架構和命名規範開始,循序漸進地加入更多規則,並在 CI/CD 中自動化執行。