背景
最近在做一个新项目的时候引入了一个架构方面的需求,就是需要检查项目的编码规范、模块分类规范、类依赖规范等,刚好接触到,正好做个调研。
很多时候,我们会制定项目的规范,例如:
- 硬性规定项目包结构中service层不能引用controller层的类(这个例子有点极端)。
- 硬性规定定义在controller包下的Controller类的类名称以"Controller"结尾,方法的入参类型命名以"Request"结尾,返回参数命名以"Response"结尾。
- 枚举类型必须放在common.constant包下,以类名称Enum结尾。
还有很多其他可能需要定制的规范,最终可能会输出一个文档。但是,谁能保证所有参数开发的人员都会按照文档的规范进行开发?为了保证规范的实行,Archunit以单元测试的形式通过扫描类路径(甚至Jar)包下的所有类,通过单元测试的形式对各个规范进行代码编写,如果项目代码中有违背对应的单测规范,那么单元测试将会不通过,这样就可以从CI/CD层面彻底把控项项目架构和编码规范。
简介
Archunit是一个免费、简单、可扩展的类库,用于检查Java代码的体系结构。提供检查包和类的依赖关系、调用层次和切面的依赖关系、循环依赖检查等其他功能。它通过导入所有类的代码结构,基于Java字节码分析实现这一点。的主要关注点是使用任何普通的Java单元测试框架自动测试代码体系结构和编码规则。
引入依赖
一般来说,目前常用的测试框架是Junit4,需要引入Junit4和archunit:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
由于-junit4
中依赖到slf4j,因此最好在测试依赖中引入一个slf4j的实现,例如logback:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
如何使用
主要从下面的两个方面介绍一下的使用:
- 指定参数进行类扫描。
- 内建规则定义。
指定参数进行类扫描
需要对代码或者依赖规则进行判断前提是要导入所有需要分析的类,类扫描导入依赖于ClassFileImporter
,底层依赖于ASM字节码框架针对类文件的字节码进行解析,性能会比基于反射的类扫描框架高很多。ClassFileImporter
的构造可选参数为ImportOption(s)
,扫描规则可以通过ImportOption
接口实现,默认提供可选的规则有:
// 不包含测试类
ImportOption.Predefined.DONT_INCLUDE_TESTS
// 不包含Jar包里面的类
ImportOption.Predefined.DONT_INCLUDE_JARS
// 不包含Jar和Jrt包里面的类,JDK9的特性
ImportOption.Predefined.DONT_INCLUDE_ARCHIVES
举个例子,我们实现一个自定义的ImportOption
实现,用于指定需要排除扫描的包路径:
public class DontIncludePackagesImportOption implements ImportOption {
private final Set<Pattern> EXCLUDED_PATTERN;
public DontIncludePackagesImportOption(String... packages) {
EXCLUDED_PATTERN = new HashSet<>(8);
for (String eachPackage : packages) {
EXCLUDED_PATTERN.add(Pattern.compile(String.format(".*/%s/.*", eachPackage.replace("/", "."))));
}
}
@Override
public boolean includes(Location location) {
for (Pattern pattern : EXCLUDED_PATTERN) {
if (location.matches(pattern)) {
return false;
}
}
return true;
}
}
ImportOption
接口只有一个方法:
boolean includes(Location location)
其中,Location
包含了路径信息、是否Jar文件等判断属性的元数据,方便使用正则表达式或者直接的逻辑判断。
接着我们可以通过上面实现的DontIncludePackagesImportOption
去构造ClassFileImporter
实例:
ImportOptions importOptions = new ImportOptions()
// 不扫描jar包
.with(ImportOption.Predefined.DONT_INCLUDE_JARS)
// 排除不扫描的包