项目架构级别规约框架Archunit调研

最近在做一个新项目的时候引入了一个架构方面的需求,就是需要检查项目的编码规范、模块分类规范、类依赖规范等,刚好接触到,正好做个调研。

很多时候,我们会制定项目的规范,例如:

硬性规定项目包结构中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)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppwdp.html