最近在做一个新项目的时候引入了一个架构方面的需求,就是需要检查项目的编码规范、模块分类规范、类依赖规范等,刚好接触到,正好做个调研。
很多时候,我们会制定项目的规范,例如:
硬性规定项目包结构中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("http://www.likecs.com/", ".")))); } } @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) // 排除不扫描的包 .with(new DontIncludePackagesImportOption("com.sample..support")); ClassFileImporter classFileImporter = new ClassFileImporter(importOptions);得到ClassFileImporter实例后我们可以通过对应的方法导入项目中的类:
// 指定类型导入单个类 public JavaClass importClass(Class<?> clazz) // 指定类型导入多个类 public JavaClasses importClasses(Class<?>... classes) public JavaClasses importClasses(Collection<Class<?>> classes) // 通过指定路径导入类 public JavaClasses importUrl(URL url) public JavaClasses importUrls(Collection<URL> urls) public JavaClasses importLocations(Collection<Location> locations) // 通过类路径导入类 public JavaClasses importClasspath() public JavaClasses importClasspath(ImportOptions options) // 通过文件路径导入类 public JavaClasses importPath(String path) public JavaClasses importPath(Path path) public JavaClasses importPaths(String... paths) public JavaClasses importPaths(Path... paths) public JavaClasses importPaths(Collection<Path> paths) // 通过Jar文件对象导入类 public JavaClasses importJar(JarFile jar) public JavaClasses importJars(JarFile... jarFiles) public JavaClasses importJars(Iterable<JarFile> jarFiles) // 通过包路径导入类 - 这个是比较常用的方法 public JavaClasses importPackages(Collection<String> packages) public JavaClasses importPackages(String... packages) public JavaClasses importPackagesOf(Class<?>... classes) public JavaClasses importPackagesOf(Collection<Class<?>> classes)