深圳幻海软件技术有限公司 欢迎您!

全面掌握软件架构的守护神-ArchUnit

2023-02-28

简要介绍ArchUnit是一个免费、简单和可扩展的库,可以使用任何普通的Java单元测试框架检查Java代码的架构和编码规则。基本原理ArchUnit通过分析给定的Java字节码,将所有类导入到Java代码结构中,来检查包、类、层、切片上依赖关系,包括对循环依赖关系等问题的检查。版本分支ArchUn

简要介绍

ArchUnit 是一个免费、简单和可扩展的库,可以使用任何普通的 Java 单元测试框架检查 Java 代码的架构和编码规则。

基本原理

ArchUnit 通过分析给定的 Java 字节码,将所有类导入到 Java 代码结构中,来检查包、类、层、切片上依赖关系,包括对循环依赖关系等问题的检查。

版本分支

ArchUnit 于2017年4月23日发布第一个版本,2022年10月3日发布了 1.0.0 版本,共32次Release。

ArchUnitNet 是一个 关于.NET/C# 的架构测试工具。

体系结构

  • 总览

ArchUnit 由 ArchUnit、 ArchUnit-junit4、ArchUnit-junit5-api、 ArchUnit-junit5-engine 和
ArchUnit-junit5-engine-api 等模块组成,还为最终用户提供了 archunit-example 模块。

  • ArchUnit

ArchUnit 模块包含编写架构测试所需的核心基础结构,如ClassFileImporter、域对象和规则语法结构。ArchUnit 分为 Core、Lang 和 Library 三层,Core 层处理基本的基础结构,比如将字节码导入为Java对象; Lang 层提供以简洁的方式制定架构规则的语法; Library 层包含更为复杂的预定义规则,如多层分层架构。

  • ArchUnit-Junit

ArchUnit-junit4 模块包含与 JUnit 4集成的基础结构,特别是用于缓存导入类的 ArchUnitRunner。

ArchUnit-junit5-* 模块包含与 JUnit 5集成的基础结构,并包含在测试运行之间缓存导入类的基础结构。ArchUnit-junit5-API 包含用户 API,用于编写支持 ArchUnit 的 JUnit 5的测试,ArchUnit-junit5-engine 包含运行这些测试的运行时引擎。
ArchUnit-junit5-engine-API 包含一些 API 代码,这些 API 代码用于那些想要对运行 ArchUnit JUnit 5测试进行更详细控制的工具,特别是一个 FieldSelector,它可以用来指示 ArchUnitTestEngine 运行一个特定的规则字段(比较 JUnit 4和5 Support)。

  • ArchUnit-Example

archunit-example 模块包含违反这些规则的示例体系结构规则和示例代码。在这里可以找到关于如何为项目设置规则的灵感,或者在 ArchUnit-最新发布版本的示例。

  • ArchUnit-Maven-Plugin

有一个maven插件arch-unit-maven-plugin,可以从 Maven 运行 ArchUnit 规则。

安装导入

要使用 ArchUnit,在类路径中包含相应的 JAR 文件就足够了。

# junit4 maven 依赖,for junit4
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

# junit5 maven 依赖,for junit5
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

快速体验
@RunWith(ArchUnitRunner.class) // Junit5不需要这行
@AnalyzeClasses(packages = "com.mycompany.myapp") // ① 导入要分析的类
public class MyArchitectureTest {

    @ArchTest // ② 方式一:使用静态字段,对要分析的类的架构规则进行断言
    public static final ArchRule myRule = classes()
        .that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

    @Test // ② 方式二:使用方法,并自行导入类,对要分析的类的架构规则进行断言
    public void Services_should_only_be_accessed_by_Controllers(){
        JavaClasses importedClasses = new ClassFileImporter()
                    .importPackages("com.mycompany.myapp");
    
        ArchRule myRule = classes()
            .that().resideInAPackage("..service..")
            .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
    
        myRule.check(importedClasses);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

详细功能

  • 包依赖检查
// 不允许任何 source 包中的类依赖于 foo 包中的类
noClasses().that().resideInAPackage("..source..")
    .should().dependOnClassesThat().resideInAPackage("..foo..");
    
// foo 包中的类只能被 source.one 包和本包中的类依赖
classes().that().resideInAPackage("..foo..")
    .should().onlyHaveDependentClassesThat()
    .resideInAnyPackage("..source.one..", "..foo..")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 类依赖检查

// 名为 *Bar 的类只能被名为 Bar 的类依赖 
classes().that().haveNameMatching(".*Bar")
    .should().onlyHaveDependentClassesThat().haveSimpleName("Bar")
  • 1.
  • 2.
  • 3.

  • 类容器检查

// Foo 开头的类只能放在 com.foo 包下
classes().that().haveSimpleNameStartingWith("Foo")
    .should().resideInAPackage("com.foo")
  • 1.
  • 2.
  • 3.

  • 类继承检查
// 实现 Connection 接口的类名称只能以 Connection 结尾
classes().that().implement(Connection.class)
    .should().haveSimpleNameEndingWith("Connection")

// 用到 EntityManager 的类只能在 persistence 包下    
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyHaveDependentClassesThat()
    .resideInAnyPackage("..persistence..")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 注解检查
// 用到 EntityManager 的类需要依赖于 Transactional 注解
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyHaveDependentClassesThat()
    .areAnnotatedWith(Transactional.class)
  • 1.
  • 2.
  • 3.
  • 4.
  • 分层检查

layeredArchitecture()
    .consideringAllDependencies()
    .layer("Controller").definedBy("..controller..") 
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Controller").mayNotBeAccessedByAnyLayer() // controller层不能被其它层访问
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller") // service层只能被controller层访问
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service") // persistence层只能被service层访问
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

  • 循环依赖检查

// com.myapp 的直属子包间不能存在循环依赖
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
  • 1.
  • 2.

深入了解

  • 导入

//使用预定义导入选项从classpath导入类
new ClassFileImporter()
    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
    .importClasspath();
    
//从文件路径导入类
JavaClasses classes = new ClassFileImporter().importPath("/some/path/to/classes");

// 自定义导入选项,以忽略测试类
ImportOption ignoreTests = new ImportOption() {
    @Override
    public boolean includes(Location location){
        return !location.contains("/test/"); // ignore any URI to sources that contains '/test/'
    }
};
// 使用自定义规则从classpath导入类
JavaClasses classes = new ClassFileImporter()
        .withImportOption(ignoreTests).importClasspath();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 概念模型

大多数对象类似于 Java 反射 API,包括继承关系。因此,一个 JavaClass 具有一些 JavaMember,JavaMember 可以是 JavaField、 JavaMethod、 JavaConstruction (或 JavaStaticInitializer)。

CodeUnit 虽然不存在于反射 API 中,但是为任何可以访问其他代码的东西引入一个概念是有意义的。它要么是一个方法,一个构造函数(包括类初始化器) ,要么是一个类的静态初始化器(例如静态块,静态字段赋值,等等)。

对另一个类的访问也是一个不在反射范畴的概念,ArchUnit在最细粒度上,只能从 CodeUnit 通过 JavaFieldAccess 、JavaMethodCall、JavaConstructorCall 来分别访问字段、方法或构造函数。

由于被访问的字段、方法、构造函数可能定义在超类中,所以引入了FieldAccessTarget、MethodCallTarget、ConstructorCallTarget等Target系列概念,用于解析到真正的目标类。

由于导入的类集并不总是包含所有的类,所以上图中resolves to可能解析到0个对象。

另外,上图中MethodCallTarget可以resolves to多个JavaMethod,其原因在于某个方法可能实现了多个接口,如下图所示。

  • Core API 和 Lang API

Core API具备强大的功能,但是Lang API更为简洁。

// 本段代码为使用Core API断言规则
Set<JavaClass> services = new HashSet<>();
for (JavaClass clazz : classes) {
    // choose those classes with FQN with infix '.service.'
    if (clazz.getName().contains(".service.")) {
        services.add(clazz);
    }
}

for (JavaClass service : services) {
    for (JavaAccess<?> access : service.getAccessesFromSelf()) {
        String targetName = access.getTargetOwner().getName();

        // fail if the target FQN has the infix ".controller."
        if (targetName.contains(".controller.")) {
            String message = String.format(
                    "Service %s accesses Controller %s in line %d",
                    service.getName(), targetName, access.getLineNumber());
            Assert.fail(message);
        }
    }
}

// 如下代码片段为使用Lang API实现如上相同的规则断言
ArchRule rule = ArchRuleDefinition.noClasses()
    .that().resideInAPackage("..service..")
    .should().accessClassesThat().resideInAPackage("..controller..");

rule.check(importedClasses);

// 如下代码展示Lang API提供的 and、or 等组合功能
noClasses()
    .that().resideInAPackage("..service..")
    .or().resideInAPackage("..persistence..")
    .should().accessClassesThat().resideInAPackage("..controller..")
    .orShould().accessClassesThat().resideInAPackage("..ui..")

rule.check(importedClasses);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

Lang 层除了为类提供API之外,还为其成员提供了正反两个系列的API,包括members()、noMembers()、fields()、noFields()、codeUnits()、noCodeUnits()、constructors()、noConstructors()等。

// 如下代码片段展示与成员方法有关的API
ArchRule rule = ArchRuleDefinition.methods()
    .that().arePublic()
    .and().areDeclaredInClassesThat().resideInAPackage("..controller..")
    .should().beAnnotatedWith(Secured.class);

rule.check(importedClasses);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

  • 自定义规则

在ArchUnit,大多数规则都是如下架构。

classes that ${PREDICATE} should ${CONDITION}
  • 1.

如果预定义API不能满足要求,可以自定义规则。

// 定义一个 Predicate
DescribedPredicate<JavaClass> haveAFieldAnnotatedWithPayload =
    new DescribedPredicate<JavaClass>("have a field annotated with @Payload"){
        @Override
        public boolean apply(JavaClass input){
            boolean someFieldAnnotatedWithPayload = // iterate fields and check for @Payload
            return someFieldAnnotatedWithPayload;
        }
    };

// 定义一个Condition
ArchCondition<JavaClass> onlyBeAccessedBySecuredMethods =
    new ArchCondition<JavaClass>("only be accessed by @Secured methods") {
        @Override
        public void check(JavaClass item, ConditionEvents events){
            for (JavaMethodCall call : item.getMethodCallsToSelf()) {
                if (!call.getOrigin().isAnnotatedWith(Secured.class)) {
                    String message = String.format(
                        "Method %s is not @Secured", call.getOrigin().getFullName());
                    events.add(SimpleConditionEvent.violated(call, message));
                }
            }
        }
    };

// 对类集应用 Predicate 和 Condition
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

  • 控制规则文案

// 对于不常见的规则,最好按照如下方法记录其理由
classes().that(haveAFieldAnnotatedWithPayload)
        .should(onlyBeAccessedBySecuredMethods)
        .because("@Secured methods will be intercepted, checking for increased privileges " +
        "and obfuscating sensitive auditing information");
        
// 如果规则复杂,且自动生成的规则文本太复杂,可以使用如下方式完全覆盖规则说明
classes().that(haveAFieldAnnotatedWithPayload)
    .should(onlyBeAccessedBySecuredMethods)
    .as("Payload may only be accessed in a secure way");
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

  • 忽略违规情况

因遗留代码或其它无法满足规则的情况,可以将一个名为
archunit_ignore_patterns.txt 的文本文件放在classpath的根目录下,并在每一行使用一个可以匹配要忽略的冲突的正则表达式。

# 这里可以写上忽略的原因
.*some\.pkg\.LegacyService.*
  • 1.
  • 2.

  • 架构检查

ArchUnit 在 Library 层预定义了若干架构检查的 API。目前可以方便地检查分层架构和洋葱架构,将来可能会扩展到管道、过滤器,以及业务和技术关注点分离等。

// 架构检查的入口点
com.tngtech.archunit.library.Architectures

// 以下是对分层架构的检查示例
layeredArchitecture()
    .consideringAllDependencies()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
    
// 以下为对洋葱架构(又称六边形架构、端口和适配器架构)的检查示例
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..");
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

  • 切片检查
// 切片检查的入口点
com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
 
// 检查 myapp 包的下一级子包中的类不存在循环依赖
SlicesRuleDefinition.slices()
    .matching("..myapp.(*)..")
    .should().beFreeOfCycles()

// 检查 myapp 包的所有子包中的类不存在循环依赖
SlicesRuleDefinition.slices()
    .matching("..myapp.(**)")
    .should().notDependOnEachOther()

// 检查 myapp 包和 service 包之间的包中的类不存在相互依赖情况
SlicesRuleDefinition.slices()
    .matching("..myapp.(**).service..")
    .should().notDependOnEachOther()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

如果以上切片不能满足要求,还可以使用SliceAssignment类来定制切片。

  • 编码检查

ArchUnit 通过 GeneralCodingRules 类提供了一组通用性较高的编码规则检查。

  • 依赖检查

DependencyRules 类提供了一组检查类之间依赖关系的规则和条件。

  • 代理检查

ProxyRules 提供了关于使用代理对象的检查。

  • 基于PlantUML检查

ArchUnit 在
com.tngtech.archunit.library.plantuml 包下提供了一个支持 PlantUML 的特性,用于直接从 PlantUML 派生出检查规则,对相应的类进行检查。

URL myDiagram = getClass().getResource("my-diagram.puml");

classes().should(
        adhereToPlantUmlDiagram(myDiagram, 
            consideringAllDependencies())
    );
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

支持的UML只能是组件图,用Java类的Package作为组件原型。ArchUnit对组件图还有一些特殊要求,同时提供一些检查的额外选项。

' 如果使用如下组件图进行检查,target中的类依赖source中的类将违反规则
@startuml
[某个源组件] <<..some.source..>>
[某个目标组件] <<..some.target..>> as target

[某个源组件] --> target
@enduml
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

  • 冻结

当违规行为过多,无法立即修复时,需要建立一种迭代机制,防止代码基线进一步恶化。

ArchUnit 的 FreezingArchRule 类提供这方面的帮助,将现有违规行为记录到 ViationStore 中,然后后续检查只报告新增的违规行为,并忽略已知的违规。一旦违规得到修复,FreezingArchRule 将自动将其从已知冲突中去除,无需额外回归。代码行号的变化不会影响违规行为。

// 冻结某个规则
ArchRule rule = FreezingArchRule
    .freeze(classes().should()./*complete ArchRule*/);
  • 1.
  • 2.
  • 3.

FreezingArchRule 默认使用一个简单的纯文本文件保存 ViationStore,以便利用 VCS 进行跟踪管理。该文件的路径包括 ViationStore 的创建和更新行为也是可配置的,方便用于CI环境。

# ViationStore 文件
freeze.store.default.path=/some/path/in/a/vcs/repo
# 是否允许创建 ViationStore,默认为 false
freeze.store.default.allowStoreCreatinotallow=true
# 是否允许更新 ViationStore,默认为 true
freeze.store.default.allowStoreUpdate=false

# 是否允许重新冻结所有违规行为,表示随时接受新的违规而只报告成功,默认为 false
freeze.refreeze=true

# 支持自定义冻结存储(继承com.tngtech.archunit.library.freeze.ViolationStore)
freeze.store=fully.qualified.name.of.MyCustomViolationStore
# 如下行用于为自定义存储类设置属性
freeze.store.propOne=valueOne
freeze.store.propTwo=valueTwo 

# 支持自定义违规行匹配器
freeze.lineMatcher=fully.qualified.name.of.MyCustomLineMatcher
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 度量

与代码质量度量(如圈复杂度或方法长度)类似,软件架构度量力求度量软件的结构和设计。

ArchUnit 可以用来计算一些众所周知的软件体系结构度量。

import com.tngtech.archunit.library.metrics.ArchitectureMetrics;
// ...

JavaClasses classes = // ...
Set<JavaPackage> packages = classes.getPackage("com.example").getSubpackages();

// These components can also be created in a package agnostic way, compare MetricsComponents.from(..)
MetricsComponents<JavaClass> components = MetricsComponents.fromPackages(packages);

// 计算 John Lakos 提出的依赖度量指标,指示系统组件间依赖程度
LakosMetrics metrics = ArchitectureMetrics.lakosMetrics(components);
// CCD 累积组件依赖,加总所有组件所有向外依赖数
System.out.println("CCD: " + metrics.getCumulativeComponentDependency());
// ACD 平均组件依赖,CCD除以组件数
System.out.println("ACD: " + metrics.getAverageComponentDependency());
// RACD 相对平均依赖,ACD除以组件数
System.out.println("RACD: " + metrics.getRelativeAverageComponentDependency());
// NCCD 系统的 CCD 除以具有相同数量成分的平衡二叉搜索树的 CCD
System.out.println("NCCD: " + metrics.getNormalizedCumulativeComponentDependency());

// 计算 Robert C. Martin 提出的度量指标,指示组件之间的耦合度
ComponentDependencyMetrics metrics = ArchitectureMetrics.componentDependencyMetrics(components);
//CE 传出耦合,对任何其它组件的依赖数
System.out.println("Ce: " + metrics.getEfferentCoupling("com.example.component"));
//CA 传入耦合,来自任何其它组件的依赖数
System.out.println("Ca: " + metrics.getAfferentCoupling("com.example.component"));
// I 不稳定性,Ce/(Ca + Ce) 
System.out.println("I: " + metrics.getInstability("com.example.component"));
// A 抽象性,组件内抽象类的数量 / 组件中所有类的数量
// 在 ArchUnit 中,抽象值仅基于公共类,即从外部可见的类。
System.out.println("A: " + metrics.getAbstractness("com.example.component"));
// D 距离主序列, | A + I - 1 |, 即距离(A = 1,I = 0)(A = 0,I = 1)之间的理想线的归一化距离
System.out.println("D: " + metrics.getNormalizedDistanceFromMainSequence("com.example.component"));

// 计算 Herbert Dowalil 提出的可见性指标,指示组件的信息隐藏能力
VisibilityMetrics metrics = ArchitectureMetrics.visibilityMetrics(components);
// RV 相对可见性,当前组件中可见元素数量 / 当前组件中所有元素数量
System.out.println("RV : " + metrics.getRelativeVisibility("com.example.component"));
// ARV 平均相对能见度,RV的均值 
System.out.println("ARV: " + metrics.getAverageRelativeVisibility());
// GRV 全局相对可见性,所有组件中的可见元素数量 / 所有组件中素有元素数量
System.out.println("GRV: " + metrics.getGlobalRelativeVisibility());
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

  • JUnit支持

// 以下为基本用法,项目太大时,会因为类的导入导致性能较差,也容易出错
@Test
public void rule1(){
    JavaClasses importedClasses = new ClassFileImporter().importClasspath();

    ArchRule rule = classes()...

    rule.check(importedClasses);
}

// 以下为正常用法
// 缓存基于测试类,同一个测试类中声明的多个规则重用缓存
// 缓存基于导入位置,从相同URI导入时会发生重用,这种形式为软引用,内存不足时会被清除
@RunWith(ArchUnitRunner.class) // 此行JUnit5不需要
@AnalyzeClasses(packages = "com.myapp")
public class ArchitectureTest {

    // 可将规则声明为静态字段
    @ArchTest
    public static final ArchRule rule1 = classes().should()...

    @ArchTest
    public static final ArchRule rule2 = classes().should()...

    @ArchTest
    public static void rule3(JavaClasses classes){
        // 静态方法,会使用缓存
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

  • 控制导入范围

// 控制要导入的类
@AnalyzeClasses(packages = {"com.myapp.subone", "com.myapp.subtwo"})

// 也可以利用具有代表性的类,会导入该类所在包的所有类,这种方式便于重构
@AnalyzeClasses(packagesOf = {SubOneConfiguration.class, SubTwoConfiguration.class})

// 也可以通过实现 LocationProvider 来控制要导入哪些类
public class MyLocationProvider implements LocationProvider {
    @Override
    public Set<Location> get(Class<?> testClass) {
        // Determine Locations (= URLs) to import
        // Can also consider the actual test class, e.g. to read some custom annotation
    }
}
@AnalyzeClasses(locations = MyLocationProvider.class)  

// 可以利用导入选项控制要导入的类。导入选项类也支持自定义。
@AnalyzeClasses(importOptions = {DoNotIncludeTests.class,
                                 DoNotIncludeJars.class})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

  • 禁用位置缓存

// 缓存过多的类可能会产生GC延迟。
// 可以通过 CacheMode.PER_CLASS 只启用基于测试类的缓存,
// 而禁用基于位置的缓存
@AnalyzeClasses(packages = "com.myapp.special",
          cacheMode = CacheMode.PER_CLASS)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

  • 忽略测试

可使用 @ArchIgnore 注解来忽略对规则的检查。

public class ArchitectureTest {

    // 会运行
    @ArchTest
    public static final ArchRule rule1 = classes().should()...

    // 不会运行
    @ArchIgnore
    @ArchTest
    public static final ArchRule rule2 = classes().should()...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 规则分组

可使用如下方式对检查规则进行分组,提升规则的组织性,并允许在项目或模块间复用规则。

public class ServiceRules {
    @ArchTest
    public static final ArchRule ruleOne = ...

    // 其他规则
}

public class PersistenceRules {
    @ArchTest
    public static final ArchRule ruleOne = ...

    // 更多规则
}

@RunWith(ArchUnitRunner.class) // Junit5不需要此行
@AnalyzeClasses
public class ArchitectureTest {

    @ArchTest
    static final ArchTests serviceRules = ArchTests.in(ServiceRules.class);

    @ArchTest
    static final ArchTests persistenceRules = ArchTests.in(PersistenceRules.class);

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

  • 生成显示名称

ArchUnit 通过使用空格替换原始规则名称中的下划线,提供了在测试报告中生成更多可读名称的可能性。

// 如果方法或字段的名称为:
some_Field_or_Method_rule
// 则在测试报告中会被替换为:
some Field or Method rule
  • 1.
  • 2.
  • 3.
  • 4.

也可以将下面的属性移除或设置为false来关闭该特性。

junit.displayName.replaceUnderscoresBySpaces=true
  • 1.

高级配置

  • 覆盖默认配置

ArchUnit 会使用 Java 资源标准加载机制来加载classpath根下的 archunit.properties 配置文件。

可以通过将系统属性传递给执行ArchUnit的JVM进程来覆盖 archunit.properties 中的配置。

-Darchunit.propertyName=propertyValue
  • 1.

  • 对缺失类的处理

# 不要从类路径中解析缺失的类依赖,以提升分析性能(默认行为)
resolveMissingDependenciesFromClassPath=false

# 只从类路径解析 some.pkg.[one, two] 两个包的类,其它则使用存根
classResolver=com.tngtech.archunit.core.importer.resolvers.SelectedClassResolverFromClasspath
classResolver.args=some.pkg.one,some.pkg.two
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

  • 导入不在类路径上的其它源中的类
# 从以下类继承一个自定义类解析器,如:some.pkg.MyCustomClassResolver
com.tngtech.archunit.core.importer.resolvers.ClassResolver
 
 # 在 archunit.properties 中配置自定义类解析器
 classResolver=some.pkg.MyCustomClassResolver
 
 # 为自定义类解析器提供所需参数
 classResolver.args=myArgOne,myArgTwo
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 自定义分析迭代次数

// 以下是默认迭代次数,设置为 -1 表示直到解析出所有类型才停止。
// 设置为 0 将禁用自动解析,类信息可能会不完整或错误
// 较大或负值可能对性能产生巨大影响

#字段类型、方法参数类型等成员类型
import.dependencyResolutionProcess.maxIterationsForMemberTypes = 1 
# 访问类型,如一个方法调用其它类中的方法
import.dependencyResolutionProcess.maxIterationsForAccessesToTypes = 1
# 超类,如被继承的类、被实现的接口
import.dependencyResolutionProcess.maxIterationsForSupertypes = -1
# 封闭类型,如嵌套类的外部类
import.dependencyResolutionProcess.maxIterationsForEnclosingTypes = -1
# 注解类型,包括注解的参数
import.dependencyResolutionProcess.maxIterationsForAnnotationTypes = -1
# 泛型签名类型,如包含参数化类型的类型
import.dependencyResolutionProcess.maxIterationsForGenericSignatureTypes = -1
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

  • 控制类 MD5 摘要

# 通过以下方式激活类 MD5(默认为false,以提升性能) ,用于跟踪异常行为
enableMd5InClassSources=true

# 获取类 MD5 的方法如下
javaClass.getSource().get().getMd5sum()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

  • 控制空检查
// ArchUnit 默认禁止对一组空类进行检查

# 可以针对单个规则应用 allowEmptyShould(true) 来覆盖对空检查的默认行为
classes().that()...should()...allowEmptyShould(true)
 
 # 允许全局空检查
 archRule.failOnEmptyShould=false
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 控制循环依赖检查

# 限制循环依赖检查最大周期,默认为100
cycles.maxNumberToDetect=50

# 限制每个周期报告的依赖数量(无论如何总是检查所有依赖),只影响所需的堆大小,默认为20
cycles.maxNumberOfDependenciesPerEdge=5
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

  • 自定义错误消息
# 首先通过继承下面的类来实现一个自定义错误消息的类,如:some.pkg.MyCustomFailureDisplayFormat
com.tngtech.archunit.lang.FailureDisplayFormat
 
# 然后在archunit.properties中作如下配置
failureDisplayFormat=some.pkg.MyCustomFailureDisplayFormat
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.