@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);}}
//使用预定义导入选项从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();
// 本段代码为使用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()等。
// 定义一个 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");
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})
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);}