新聞中心
簡要介紹
ArchUnit 是一個免費(fèi)、簡單和可擴(kuò)展的庫,可以使用任何普通的 Java 單元測試框架檢查 Java 代碼的架構(gòu)和編碼規(guī)則。

基本原理
ArchUnit 通過分析給定的 Java 字節(jié)碼,將所有類導(dǎo)入到 Java 代碼結(jié)構(gòu)中,來檢查包、類、層、切片上依賴關(guān)系,包括對循環(huán)依賴關(guān)系等問題的檢查。
版本分支
ArchUnit 于2017年4月23日發(fā)布第一個版本,2022年10月3日發(fā)布了 1.0.0 版本,共32次Release。
ArchUnitNet 是一個 關(guān)于.NET/C# 的架構(gòu)測試工具。
體系結(jié)構(gòu)
- 總覽
ArchUnit 由 ArchUnit、 ArchUnit-junit4、ArchUnit-junit5-api、 ArchUnit-junit5-engine 和
ArchUnit-junit5-engine-api 等模塊組成,還為最終用戶提供了 archunit-example 模塊。
- ArchUnit
ArchUnit 模塊包含編寫架構(gòu)測試所需的核心基礎(chǔ)結(jié)構(gòu),如ClassFileImporter、域?qū)ο蠛鸵?guī)則語法結(jié)構(gòu)。ArchUnit 分為 Core、Lang 和 Library 三層,Core 層處理基本的基礎(chǔ)結(jié)構(gòu),比如將字節(jié)碼導(dǎo)入為Java對象; Lang 層提供以簡潔的方式制定架構(gòu)規(guī)則的語法; Library 層包含更為復(fù)雜的預(yù)定義規(guī)則,如多層分層架構(gòu)。
- ArchUnit-Junit
ArchUnit-junit4 模塊包含與 JUnit 4集成的基礎(chǔ)結(jié)構(gòu),特別是用于緩存導(dǎo)入類的 ArchUnitRunner。
ArchUnit-junit5-* 模塊包含與 JUnit 5集成的基礎(chǔ)結(jié)構(gòu),并包含在測試運(yùn)行之間緩存導(dǎo)入類的基礎(chǔ)結(jié)構(gòu)。ArchUnit-junit5-API 包含用戶 API,用于編寫支持 ArchUnit 的 JUnit 5的測試,ArchUnit-junit5-engine 包含運(yùn)行這些測試的運(yùn)行時引擎。
ArchUnit-junit5-engine-API 包含一些 API 代碼,這些 API 代碼用于那些想要對運(yùn)行 ArchUnit JUnit 5測試進(jìn)行更詳細(xì)控制的工具,特別是一個 FieldSelector,它可以用來指示 ArchUnitTestEngine 運(yùn)行一個特定的規(guī)則字段(比較 JUnit 4和5 Support)。
- ArchUnit-Example
archunit-example 模塊包含違反這些規(guī)則的示例體系結(jié)構(gòu)規(guī)則和示例代碼。在這里可以找到關(guān)于如何為項(xiàng)目設(shè)置規(guī)則的靈感,或者在 ArchUnit-最新發(fā)布版本的示例。
- ArchUnit-Maven-Plugin
有一個maven插件arch-unit-maven-plugin,可以從 Maven 運(yùn)行 ArchUnit 規(guī)則。
安裝導(dǎo)入
要使用 ArchUnit,在類路徑中包含相應(yīng)的 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
快速體驗(yàn)
@RunWith(ArchUnitRunner.class) // Junit5不需要這行
@AnalyzeClasses(packages = "com.mycompany.myapp") // ① 導(dǎo)入要分析的類
public class MyArchitectureTest {
@ArchTest // ② 方式一:使用靜態(tài)字段,對要分析的類的架構(gòu)規(guī)則進(jìn)行斷言
public static final ArchRule myRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
@Test // ② 方式二:使用方法,并自行導(dǎo)入類,對要分析的類的架構(gòu)規(guī)則進(jìn)行斷言
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);
}
}
詳細(xì)功能
- 包依賴檢查
// 不允許任何 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")
- 類繼承檢查
// 實(shí)現(xiàn) Connection 接口的類名稱只能以 Connection 結(jié)尾
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層訪問
- 循環(huán)依賴檢查
// com.myapp 的直屬子包間不能存在循環(huán)依賴
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
深入了解
- 導(dǎo)入
//使用預(yù)定義導(dǎo)入選項(xiàng)從classpath導(dǎo)入類
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importClasspath();
//從文件路徑導(dǎo)入類
JavaClasses classes = new ClassFileImporter().importPath("/some/path/to/classes");
// 自定義導(dǎo)入選項(xiàng),以忽略測試類
ImportOption ignoreTests = new ImportOption() {
@Override
public boolean includes(Location location){
return !location.contains("/test/"); // ignore any URI to sources that contains '/test/'
}
};
// 使用自定義規(guī)則從classpath導(dǎo)入類
JavaClasses classes = new ClassFileImporter()
.withImportOption(ignoreTests).importClasspath();
- 概念模型
大多數(shù)對象類似于 Java 反射 API,包括繼承關(guān)系。因此,一個 JavaClass 具有一些 JavaMember,JavaMember 可以是 JavaField、 JavaMethod、 JavaConstruction (或 JavaStaticInitializer)。
CodeUnit 雖然不存在于反射 API 中,但是為任何可以訪問其他代碼的東西引入一個概念是有意義的。它要么是一個方法,一個構(gòu)造函數(shù)(包括類初始化器) ,要么是一個類的靜態(tài)初始化器(例如靜態(tài)塊,靜態(tài)字段賦值,等等)。
對另一個類的訪問也是一個不在反射范疇的概念,ArchUnit在最細(xì)粒度上,只能從 CodeUnit 通過 JavaFieldAccess 、JavaMethodCall、JavaConstructorCall 來分別訪問字段、方法或構(gòu)造函數(shù)。
由于被訪問的字段、方法、構(gòu)造函數(shù)可能定義在超類中,所以引入了FieldAccessTarget、MethodCallTarget、ConstructorCallTarget等Target系列概念,用于解析到真正的目標(biāo)類。
由于導(dǎo)入的類集并不總是包含所有的類,所以上圖中resolves to可能解析到0個對象。
另外,上圖中MethodCallTarget可以resolves to多個JavaMethod,其原因在于某個方法可能實(shí)現(xiàn)了多個接口,如下圖所示。
- Core API 和 Lang API
Core API具備強(qiáng)大的功能,但是Lang API更為簡潔。
// 本段代碼為使用Core API斷言規(guī)則
Setservices = 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實(shí)現(xiàn)如上相同的規(guī)則斷言
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()等。
// 如下代碼片段展示與成員方法有關(guān)的API
ArchRule rule = ArchRuleDefinition.methods()
.that().arePublic()
.and().areDeclaredInClassesThat().resideInAPackage("..controller..")
.should().beAnnotatedWith(Secured.class);
rule.check(importedClasses);
- 自定義規(guī)則
在ArchUnit,大多數(shù)規(guī)則都是如下架構(gòu)。
classes that ${PREDICATE} should ${CONDITION}如果預(yù)定義API不能滿足要求,可以自定義規(guī)則。
// 定義一個 Predicate
DescribedPredicatehaveAFieldAnnotatedWithPayload =
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
ArchConditiononlyBeAccessedBySecuredMethods =
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));
}
}
}
};
// 對類集應(yīng)用 Predicate 和 Condition
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);
- 控制規(guī)則文案
// 對于不常見的規(guī)則,最好按照如下方法記錄其理由
classes().that(haveAFieldAnnotatedWithPayload)
.should(onlyBeAccessedBySecuredMethods)
.because("@Secured methods will be intercepted, checking for increased privileges " +
"and obfuscating sensitive auditing information");
// 如果規(guī)則復(fù)雜,且自動生成的規(guī)則文本太復(fù)雜,可以使用如下方式完全覆蓋規(guī)則說明
classes().that(haveAFieldAnnotatedWithPayload)
.should(onlyBeAccessedBySecuredMethods)
.as("Payload may only be accessed in a secure way");
- 忽略違規(guī)情況
因遺留代碼或其它無法滿足規(guī)則的情況,可以將一個名為
archunit_ignore_patterns.txt 的文本文件放在classpath的根目錄下,并在每一行使用一個可以匹配要忽略的沖突的正則表達(dá)式。
# 這里可以寫上忽略的原因
.*some\.pkg\.LegacyService.*
- 架構(gòu)檢查
ArchUnit 在 Library 層預(yù)定義了若干架構(gòu)檢查的 API。目前可以方便地檢查分層架構(gòu)和洋蔥架構(gòu),將來可能會擴(kuò)展到管道、過濾器,以及業(yè)務(wù)和技術(shù)關(guān)注點(diǎn)分離等。
// 架構(gòu)檢查的入口點(diǎn)
com.tngtech.archunit.library.Architectures
// 以下是對分層架構(gòu)的檢查示例
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")
// 以下為對洋蔥架構(gòu)(又稱六邊形架構(gòu)、端口和適配器架構(gòu))的檢查示例
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..");
- 切片檢查
// 切片檢查的入口點(diǎn)
com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
// 檢查 myapp 包的下一級子包中的類不存在循環(huán)依賴
SlicesRuleDefinition.slices()
.matching("..myapp.(*)..")
.should().beFreeOfCycles()
// 檢查 myapp 包的所有子包中的類不存在循環(huán)依賴
SlicesRuleDefinition.slices()
.matching("..myapp.(**)")
.should().notDependOnEachOther()
// 檢查 myapp 包和 service 包之間的包中的類不存在相互依賴情況
SlicesRuleDefinition.slices()
.matching("..myapp.(**).service..")
.should().notDependOnEachOther()
如果以上切片不能滿足要求,還可以使用SliceAssignment類來定制切片。
- 編碼檢查
ArchUnit 通過 GeneralCodingRules 類提供了一組通用性較高的編碼規(guī)則檢查。
- 依賴檢查
DependencyRules 類提供了一組檢查類之間依賴關(guān)系的規(guī)則和條件。
- 代理檢查
ProxyRules 提供了關(guān)于使用代理對象的檢查。
- 基于PlantUML檢查
ArchUnit 在
com.tngtech.archunit.library.plantuml 包下提供了一個支持 PlantUML 的特性,用于直接從 PlantUML 派生出檢查規(guī)則,對相應(yīng)的類進(jìn)行檢查。
URL myDiagram = getClass().getResource("my-diagram.puml");
classes().should(
adhereToPlantUmlDiagram(myDiagram,
consideringAllDependencies())
);支持的UML只能是組件圖,用Java類的Package作為組件原型。ArchUnit對組件圖還有一些特殊要求,同時提供一些檢查的額外選項(xiàng)。
' 如果使用如下組件圖進(jìn)行檢查,target中的類依賴source中的類將違反規(guī)則
@startuml
[某個源組件] <<..some.source..>>
[某個目標(biāo)組件] <<..some.target..>> as target
[某個源組件] --> target
@enduml
- 凍結(jié)
當(dāng)違規(guī)行為過多,無法立即修復(fù)時,需要建立一種迭代機(jī)制,防止代碼基線進(jìn)一步惡化。
ArchUnit 的 FreezingArchRule 類提供這方面的幫助,將現(xiàn)有違規(guī)行為記錄到 ViationStore 中,然后后續(xù)檢查只報告新增的違規(guī)行為,并忽略已知的違規(guī)。一旦違規(guī)得到修復(fù),F(xiàn)reezingArchRule 將自動將其從已知沖突中去除,無需額外回歸。代碼行號的變化不會影響違規(guī)行為。
// 凍結(jié)某個規(guī)則
ArchRule rule = FreezingArchRule
.freeze(classes().should()./*complete ArchRule*/);
FreezingArchRule 默認(rèn)使用一個簡單的純文本文件保存 ViationStore,以便利用 VCS 進(jìn)行跟蹤管理。該文件的路徑包括 ViationStore 的創(chuàng)建和更新行為也是可配置的,方便用于CI環(huán)境。
# ViationStore 文件
freeze.store.default.path=/some/path/in/a/vcs/repo
# 是否允許創(chuàng)建 ViationStore,默認(rèn)為 false
freeze.store.default.allowStoreCreatinotallow=true
# 是否允許更新 ViationStore,默認(rèn)為 true
freeze.store.default.allowStoreUpdate=false
# 是否允許重新凍結(jié)所有違規(guī)行為,表示隨時接受新的違規(guī)而只報告成功,默認(rèn)為 false
freeze.refreeze=true
# 支持自定義凍結(jié)存儲(繼承com.tngtech.archunit.library.freeze.ViolationStore)
freeze.store=fully.qualified.name.of.MyCustomViolationStore
# 如下行用于為自定義存儲類設(shè)置屬性
freeze.store.propOne=valueOne
freeze.store.propTwo=valueTwo
# 支持自定義違規(guī)行匹配器
freeze.lineMatcher=fully.qualified.name.of.MyCustomLineMatcher
- 度量
與代碼質(zhì)量度量(如圈復(fù)雜度或方法長度)類似,軟件架構(gòu)度量力求度量軟件的結(jié)構(gòu)和設(shè)計(jì)。
ArchUnit 可以用來計(jì)算一些眾所周知的軟件體系結(jié)構(gòu)度量。
import com.tngtech.archunit.library.metrics.ArchitectureMetrics;
// ...
JavaClasses classes = // ...
Setpackages = classes.getPackage("com.example").getSubpackages();
// These components can also be created in a package agnostic way, compare MetricsComponents.from(..)
MetricsComponentscomponents = MetricsComponents.fromPackages(packages);
// 計(jì)算 John Lakos 提出的依賴度量指標(biāo),指示系統(tǒng)組件間依賴程度
LakosMetrics metrics = ArchitectureMetrics.lakosMetrics(components);
// CCD 累積組件依賴,加總所有組件所有向外依賴數(shù)
System.out.println("CCD: " + metrics.getCumulativeComponentDependency());
// ACD 平均組件依賴,CCD除以組件數(shù)
System.out.println("ACD: " + metrics.getAverageComponentDependency());
// RACD 相對平均依賴,ACD除以組件數(shù)
System.out.println("RACD: " + metrics.getRelativeAverageComponentDependency());
// NCCD 系統(tǒng)的 CCD 除以具有相同數(shù)量成分的平衡二叉搜索樹的 CCD
System.out.println("NCCD: " + metrics.getNormalizedCumulativeComponentDependency());
// 計(jì)算 Robert C. Martin 提出的度量指標(biāo),指示組件之間的耦合度
ComponentDependencyMetrics metrics = ArchitectureMetrics.componentDependencyMetrics(components);
//CE 傳出耦合,對任何其它組件的依賴數(shù)
System.out.println("Ce: " + metrics.getEfferentCoupling("com.cdxwcx.component"));
//CA 傳入耦合,來自任何其它組件的依賴數(shù)
System.out.println("Ca: " + metrics.getAfferentCoupling("com.cdxwcx.component"));
// I 不穩(wěn)定性,Ce/(Ca + Ce)
System.out.println("I: " + metrics.getInstability("com.cdxwcx.component"));
// A 抽象性,組件內(nèi)抽象類的數(shù)量 / 組件中所有類的數(shù)量
// 在 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"));
// 計(jì)算 Herbert Dowalil 提出的可見性指標(biāo),指示組件的信息隱藏能力
VisibilityMetrics metrics = ArchitectureMetrics.visibilityMetrics(components);
// RV 相對可見性,當(dāng)前組件中可見元素?cái)?shù)量 / 當(dāng)前組件中所有元素?cái)?shù)量
System.out.println("RV : " + metrics.getRelativeVisibility("com.cdxwcx.component"));
// ARV 平均相對能見度,RV的均值
System.out.println("ARV: " + metrics.getAverageRelativeVisibility());
// GRV 全局相對可見性,所有組件中的可見元素?cái)?shù)量 / 所有組件中素有元素?cái)?shù)量
System.out.println("GRV: " + metrics.getGlobalRelativeVisibility());
- JUnit支持
// 以下為基本用法,項(xiàng)目太大時,會因?yàn)轭惖膶?dǎo)入導(dǎo)致性能較差,也容易出錯
@Test
public void rule1(){
JavaClasses importedClasses = new ClassFileImporter().importClasspath();
ArchRule rule = classes()...
rule.check(importedClasses);
}
// 以下為正常用法
// 緩存基于測試類,同一個測試類中聲明的多個規(guī)則重用緩存
// 緩存基于導(dǎo)入位置,從相同URI導(dǎo)入時會發(fā)生重用,這種形式為軟引用,內(nèi)存不足時會被清除
@RunWith(ArchUnitRunner.class) // 此行JUnit5不需要
@AnalyzeClasses(packages = "com.myapp")
public class ArchitectureTest {
// 可將規(guī)則聲明為靜態(tài)字段
@ArchTest
public static final ArchRule rule1 = classes().should()...
@ArchTest
public static final ArchRule rule2 = classes().should()...
@ArchTest
public static void rule3(JavaClasses classes){
// 靜態(tài)方法,會使用緩存
}
}
- 控制導(dǎo)入范圍
// 控制要導(dǎo)入的類
@AnalyzeClasses(packages = {"com.myapp.subone", "com.myapp.subtwo"})
// 也可以利用具有代表性的類,會導(dǎo)入該類所在包的所有類,這種方式便于重構(gòu)
@AnalyzeClasses(packagesOf = {SubOneConfiguration.class, SubTwoConfiguration.class})
// 也可以通過實(shí)現(xiàn) LocationProvider 來控制要導(dǎo)入哪些類
public class MyLocationProvider implements LocationProvider {
@Override
public Setget(Class> testClass) {
// Determine Locations (= 網(wǎng)站題目:全面掌握軟件架構(gòu)的守護(hù)神-ArchUnit
鏈接地址:http://m.fisionsoft.com.cn/article/dhohjge.html


咨詢
建站咨詢
