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

简要介绍

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

com.tngtech.archunit
archunit-junit4
1.0.0
test


# junit5 maven 依赖,for junit5

com.tngtech.archunit
archunit-junit5
1.0.0
test

快速体验
@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);
}
}

详细功能

  • 包依赖检查
// 不允许任何 source 包中的类依赖于 foo 包中的类
noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..");

// foo 包中的类只能被 source.one 包和本包中的类依赖
classes().that().resideInAPackage("..foo..")
.should().onlyHaveDependentClassesThat()
.resideInAnyPackage("..source.one..", "..foo..")
  • 类依赖检查
// 名为 *Bar 的类只能被名为 Bar 的类依赖 
classes().that().haveNameMatching(".*Bar")
.should().onlyHaveDependentClassesThat().haveSimpleName("Bar")

  • 类容器检查
// Foo 开头的类只能放在 com.foo 包下
classes().that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo")

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

// 用到 EntityManager 的类只能在 persistence 包下
classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat()
.resideInAnyPackage("..persistence..")
  • 注解检查
// 用到 EntityManager 的类需要依赖于 Transactional 注解
classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat()
.areAnnotatedWith(Transactional.class)
  • 分层检查
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层访问

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

深入了解

  • 导入

//使用预定义导入选项从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();
  • 概念模型

大多数对象类似于 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 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);

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);

  • 自定义规则

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

classes that ${PREDICATE} should ${CONDITION}

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

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

// 定义一个Condition
ArchCondition onlyBeAccessedBySecuredMethods =
new ArchCondition("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);

  • 控制规则文案
// 对于不常见的规则,最好按照如下方法记录其理由
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");

  • 忽略违规情况

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

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

  • 架构检查

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..");

  • 切片检查
// 切片检查的入口点
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()

如果以上切片不能满足要求,还可以使用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())
);

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

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

[某个源组件] --> target
@enduml

  • 冻结

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

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

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

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
  • 度量

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

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

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

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

// These components can also be created in a package agnostic way, compare MetricsComponents.from(..)
MetricsComponents 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.cdxwcx.component"));
//CA 传入耦合,来自任何其它组件的依赖数
System.out.println("Ca: " + metrics.getAfferentCoupling("com.cdxwcx.component"));
// I 不稳定性,Ce/(Ca + Ce)
System.out.println("I: " + metrics.getInstability("com.cdxwcx.component"));
// A 抽象性,组件内抽象类的数量 / 组件中所有类的数量
// 在 ArchUnit 中,抽象值仅基于公共类,即从外部可见的类。
System.out.println("A: " + metrics.getAbstractness("com.cdxwcx.component"));
// D 距离主序列, | A + I - 1 |, 即距离(A = 1,I = 0)和(A = 0,I = 1)之间的理想线的归一化距离
System.out.println("D: " + metrics.getNormalizedDistanceFromMainSequence("com.cdxwcx.component"));

// 计算 Herbert Dowalil 提出的可见性指标,指示组件的信息隐藏能力
VisibilityMetrics metrics = ArchitectureMetrics.visibilityMetrics(components);
// RV 相对可见性,当前组件中可见元素数量 / 当前组件中所有元素数量
System.out.println("RV : " + metrics.getRelativeVisibility("com.cdxwcx.component"));
// ARV 平均相对能见度,RV的均值
System.out.println("ARV: " + metrics.getAverageRelativeVisibility());
// GRV 全局相对可见性,所有组件中的可见元素数量 / 所有组件中素有元素数量
System.out.println("GRV: " + metrics.getGlobalRelativeVisibility());

  • 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){
// 静态方法,会使用缓存
}

}

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

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

// 也可以通过实现 LocationProvider 来控制要导入哪些类
public class MyLocationProvider implements LocationProvider {
@Override
public Set get(Class testClass) {
// Determine Locations (= 分享题目:全面掌握软件架构的守护神-ArchUnit
网站路径:http://www.shufengxianlan.com/qtweb/news17/460167.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联