本文将向大家介绍一个代码结构检查的神器 - - ArchUnit。在正式介绍ArchUnit之前,先请大家思考一下:
为什么需要对代码结构进行检查或者测试?
相信大部分的的开发人员有遇到过这样的情况(尤其是在项目逐渐变大的场景下):
开始有人画了一些漂亮的架构图,展示了系统应该包含的组件以及它们应该如何交互,大家形成一个约定并达成共识。但是随着项目逐渐变得更大,一般会经历开发人员的调整,包括新开发人员的加入或者老开发人员离开去做其它项目等。当新的需求或者特性添加进来,由于开发人员的差异,可能会出现一些不可预见的违反规范的行为,如:
这些问题可能需要在代码Review的时候才会被看到,并不是一种很及时的解决方法。
一、ArchUnit简介和入门
1.1 简介
ArchUnit is a free, simple and extensible library for checking the
architecture of your Java code.
That is, ArchUnit can check dependencies between packages
and classes, layers and slices, check for cyclic dependencies
and more. It does so by analyzing given Java bytecode,
importing all classes into a Java code structure.
ArchUnit’s main focus is to automatically test architecture
and coding rules, using any plain Java unit testing framework.
from -- https://www.archunit.org/userguide/html/000_Index.html
从上述ArchUnit的官网描述可以看出,ArchUnit是一个免费、简单和可扩展的库,用于检查Java代码的结构。ArchUnit提供了包和类之间依赖关系、循环依赖等方面的检测。ArchUnit的主要目标是使用纯Java的单元测试框架来达到自动化检测代码结构和编码规则。
1.2 快速开始
如果您想直接进入第一个ArchUnit测试,请按照以下步骤操作。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.11.0</version>
<scope>test</scope>
</dependency>
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
public class MyArchitectureTest {
@Test
public void some_architecture_rule() {
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp");
ArchRule rule = classes()... // see next section
rule.check(importedClasses);
}
}
如下是一个Hello World的示例,
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
// ...
private final ClassFileImporter importer = new ClassFileImporter();
private JavaClasses classes;
@Before
public void importClasses() {
classes = importer.importClasspath(); // imports all classes from the classpath that are not from JARs
}
@Test
public void one_should_not_access_two() {
ArchRule rule = noClasses().that().resideInAPackage("..one..")
.should().accessClassesThat().resideInAPackage("..two.."); // The '..' represents a wildcard for any number of packages
rule.check(classes);
}
// ...
如果上述规则违反了,单元测试会失败并报如下错误信息:
二、典型检测示例
2.1 包依赖检测
noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..")
classes().that().resideInAPackage("..foo..")
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")
2.2类依赖检测
classes().that().haveNameMatching(".*Bar")
.should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar")
2.3 类和包的包含关系检测
classes().that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo")
2.4 继承检测
classes().that().implement(Connection.class)
.should().haveSimpleNameEndingWith("Connection")
classes().that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byAnyPackage("..persistence..")
2.5 注解检测
classes().that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)
2.6 分层检测
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
2.7 循环检测
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
三、ArchUnit API组成部分
ArchUnit主要提供的API有Core、Lang和Library等几个部分。
其中,
ArchUnit的Core层API大部分类似于Java原生反射API,例如JavaMethod和JavaField对应于原生反射中的Method和Field,它们提供了诸如getName()、getMethods()、getType()和getParameters()等方法。
ArchUnit提供了ClassFileImporter用于导入已经编译好的Java class文件:
JavaClasses classes = new ClassFileImporter()
.importPackages("com.mycompany.myapp");
Core层的API十分强大,提供了需要关于Java程序静态结构的信息,但是直接使用Core层的API对于单元测试会缺乏表现力,特别表现在架构规则方面。
ArchUnit提供了Lang层的API,它提供了一种强大的语法来以抽象的方式表达规则。Lang层的API大多数是采用流式编程方式定义方法,例如指定包定义和调用关系的规则如下:
ArchRule rule =
classes().that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
Library层API通过静态工厂方法提供了更多复杂而强大的预定义规则。
四、更多示例
4.1 创建自定义规则
ArchUnit提供了许多预定义的语法来完成访问字段、访问方法、访问包等,一般的语法结构如下:
classes that ${PREDICATE} should ${CONDITION}
如果不能满足规则,可以通过DescribedPredicate和ArchCondition来完成自定义规则,主要格式如下:
DescribedPredicate<JavaClass> resideInAPackageService = // define the predicate
ArchCondition<JavaClass> accessClassesThatResideInAPackageController = // define the condition
noClasses().that(resideInAPackageService)
.should(accessClassesThatResideInAPackageController);
一个示例:
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;
}
};
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));
}
}
}
};
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);
如果违反了上述规则,会报如下错误信息:
classes that have a field annotated with @Payload
should only be accessed by @Secured methods
4.2 忽略某些违规行为
在遗留项目中引入结构检测,可能有太多的违规行为无法一次修复,可以先对一些违规行为进行忽略。一般的做法是定义一个记录忽略规则的文件,如archunit_ignore_patterns.txt,该文件放在根路径中。
# There are many known violations where LegacyService is involved; we'll ignore them all
.*some\.pkg\.LegacyService.*
4.3 高级配置
一些行为可以在统一的配置文件中指定,配置文件必须命名为archunit.properties
, 并存放根路径下。支持的配置选项如下所示:
# E.g. if a class calls a method, but the declaring class is not within the scope of the import,
# like in a case, where a package like 'my.app' is imported, and java.lang.String#length is called.
# Should ArchUnit try to locate the missing class on the classpath and import it as well?
#
# default = false - This has a performance impact
resolveMissingDependenciesFromClassPath=true
# Extends the customizability of 'resolveMissingDependenciesFromClassPath' by allowing to specify
# a custom implementation of ClassResolver. Such a custom implementation has full control, how
# type names should be resolved against JavaClasses. SelectedClassResolverFromClasspath is one example,
# it allows to resolve some types from the classpath (based on their package, while others are
# just stubbed. E.g. if you want to resolve classes from your own app, but not from java.util..
# or similar).
#
# classResolver.args allows to configure constructor parameters, to be supplied to a constructor
# accepting a single List<String> parameter. If no arguments are configured, a default constructor
# is supported as well.
#
# default = absent - fall back to evaluating 'resolveMissingDependenciesFromClassPath'
classResolver=com.tngtech.archunit.core.importer.resolvers.SelectedClassResolverFromClasspath
classResolver.args=com.tngtech.archunit.core,com.tngtech.archunit.base
# Should ArchUnit include the MD5 sum of imported classes into the JavaClass#getSource()?
# This way failure tracking can be improved, if there are inconsistencies within the imported sources.
#
# default = false - This has a performance impact
enableMd5InClassSources=true
五、小结
本文主要对ArchUnit进行了简单的介绍。我们可以通过在项目中引入ArchUnit,对结构完成自动化检测,持续性构建。更多的内容可以到ArchUnit官网https://www.archunit.org/userguide/html/000_Index.html进行了解。
另外可能有读者疑问,为什么要使用ArchUnit呢?这个问题,其官网也给出了解释,这里就不再具体说明了。