项目架构级别规约框架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("/", "."))));
}
}
@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)
导入类的方法提供了多维度的参数,用起来会十分便捷。例如想导入com.sample
包下面的所有类,只需要这样:
public class ClassFileImporterTest {
@Test
public void testImportBootstarpClass() throws Exception {
ImportOptions importOptions = new ImportOptions()
// 不扫描jar包
.with(ImportOption.Predefined.DONT_INCLUDE_JARS)
// 排除不扫描的包
.with(new DontIncludePackagesImportOption("com.sample..support"));
ClassFileImporter classFileImporter = new ClassFileImporter(importOptions);
long start = System.currentTimeMillis();
JavaClasses javaClasses = classFileImporter.importPackages("com.sample");
long end = System.currentTimeMillis();
System.out.println(String.format("Found %d classes,cost %d ms", javaClasses.size(), end - start));
}
}
得到的JavaClasses
是JavaClass
的集合,可以简单类比为反射中Class
的集合,后面使用的代码规则和依赖规则判断都是强依赖于JavaClasses
或者JavaClass
。
内建规则定义
类扫描和类导入完成之后,我们需要定检查规则,然后应用于所有导入的类,这样子就能完成对所有的类进行规则的过滤 - 或者说把规则应用于所有类并且进行断言。
规则定义依赖于ArchRuleDefinition
类,创建出来的规则是ArchRule
实例,规则实例的创建过程一般使用ArchRuleDefinition
类的流式方法,这些流式方法定义上符合人类思考的思维逻辑,上手比较简单,举个例子:
ArchRule archRule = ArchRuleDefinition.noClasses()
// 在service包下的所有类
.that().resideInAPackage("..service..")
// 不能调用controller包下的任意类
.should().accessClassesThat().resideInAPackage("..controller..")
// 断言描述 - 不满足规则的时候打印出来的原因
.because("不能在service包中调用controller中的类");
// 对所有的JavaClasses进行判断
archRule.check(classes);
上面展示了自定义新的ArchRule
的例子,中已经为我们内置了一些常用的ArchRule
实现,它们位于GeneralCodingRules
中:
- NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS:不能调用System.out、System.err或者(Exception.)printStackTrace。
- NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS:类不能直接抛出通用异常Throwable、Exception或者RuntimeException。
- NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING:不能使用
java.util.logging
包路径下的日志组件。
更多内建的ArchRule
或者通用的内置规则使用,可以参考官方例子。
基本使用例子
基本使用例子,主要从一些常见的编码规范或者项目规范编写规则对项目所有类进行检查。
包依赖关系检查
ArchRule archRule = ArchRuleDefinition.noClasses()
.that().resideInAPackage("..com.source..")
.should().dependOnClassesThat().resideInAPackage("..com.target..");
ArchRule archRule = ArchRuleDefinition.classes()
.that().resideInAPackage("..com.foo..")
.should().onlyAccessClassesThat().resideInAnyPackage("..com.source..", "..com.foo..");
类依赖关系检查
ArchRule archRule = ArchRuleDefinition.classes()
.that().haveNameMatching(".*Bar")
.should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar");
类包含于包的关系检查
ArchRule archRule = ArchRuleDefinition.classes()
.that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo");
继承关系检查
ArchRule archRule = ArchRuleDefinition.classes()
.that().implement(Collection.class)
.should().haveSimpleNameEndingWith("Connection");
ArchRule archRule = ArchRuleDefinition.classes()
.that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byAnyPackage("..persistence..");
注解检查
ArchRule archRule = ArchRuleDefinition.classes()
.that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)
逻辑层调用关系检查
例如项目结构如下:
- com.myapp.controller
SomeControllerOne.class
SomeControllerTwo.class
- com.myapp.service
SomeServiceOne.class
SomeServiceTwo.class
- com.myapp.persistence
SomePersistenceManager
例如我们规定:
- 包路径
com.myapp.controller
中的类不能被其他层级包引用。 - 包路径
com.myapp.service
中的类只能被com.myapp.controller
中的类引用。 - 包路径
com.myapp.persistence
中的类只能被com.myapp.service
中的类引用。
编写规则如下:
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
循环依赖关系检查
例如项目结构如下:
- com.myapp.moduleone
ClassOneInModuleOne.class
ClassTwoInModuleOne.class
- com.myapp.moduletwo
ClassOneInModuleTwo.class
ClassTwoInModuleTwo.class
- com.myapp.modulethree
ClassOneInModuleThree.class
ClassTwoInModuleThree.class
例如我们规定:com.myapp.moduleone
、com.myapp.moduletwo
和com.myapp.modulethree
三个包路径中的类不能形成一个循环依赖缓,例如:
ClassOneInModuleOne -> ClassOneInModuleTwo -> ClassOneInModuleThree -> ClassOneInModuleOne
编写规则如下:
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
核心API
把API分为三层,最重要的是"Core"层、"Lang"层和"Library"层。
Core层API
ArchUnit的Core层API大部分类似于Java原生反射API,例如JavaMethod
和JavaField
对应于原生反射中的Method
和Field
,它们提供了诸如getName()
、getMethods()
、getType()
和getParameters()
等方法。
此外ArchUnit扩展一些API用于描述依赖代码之间关系,例如JavaMethodCall
, JavaConstructorCall
或JavaFieldAccess
。还提供了例如Java类与其他Java类之间的导入访问关系的API如JavaClass#getAccessesFromSelf()
。
而需要导入类路径下或者Jar包中已经编译好的Java类,ArchUnit提供了ClassFileImporter
完成此功能:
JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");
Lang层API
Core层的API十分强大,提供了需要关于Java程序静态结构的信息,但是直接使用Core层的API对于单元测试会缺乏表现力,特别表现在架构规则方面。
出于这个原因,ArchUnit提供了Lang层的API,它提供了一种强大的语法来以抽象的方式表达规则。Lang层的API大多数是采用流式编程方式定义方法,例如指定包定义和调用关系的规则如下:
ArchRule rule =
classes()
// 定义在service包下的所欲类
.that().resideInAPackage("..service..")
// 只能被controller包或者service包中的类访问
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
编写好规则后就可以基于导入所有编译好的类进行扫描:
JavaClasses classes = new ClassFileImporter().importPackage("com.myapp");
ArchRule rule = // 定义的规则
rule.check(classes);
Library层API
Library层API通过静态工厂方法提供了更多复杂而强大的预定义规则,入口类是:
com.tngtech.archunit.library.Architectures
目前,这只能为分层架构提供方便的检查,但将来可能会扩展为六边形架构\管道和过滤器,业务逻辑和技术基础架构的分离等样式。
还有其他几个相对强大的功能:
- 代码切片功能,入口是
com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
。 - 一般编码规则,入口是
com.tngtech.archunit.library.GeneralCodingRules
。 - PlantUML组件支持,功能位于包路径
com.tngtech.archunit.library.plantuml
下。
编写复杂的规则
一般来说,内建的规则不一定能够满足一些复杂的规范校验规则,因此需要编写自定义的规则。这里仅仅举一个前文提到的相对复杂的规则:
- 定义在controller包下的Controller类的类名称以"Controller"结尾,方法的入参类型命名以"Request"结尾,返回参数命名以"Response"结尾。
官方提供的自定义规则的例子如下:
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);
我们只需要模仿它的实现即可,具体如下:
public class ArchunitTest {
@Test
public void controller_class_rule() {
JavaClasses classes = new ClassFileImporter().importPackages("club.throwable");
DescribedPredicate<JavaClass> predicate =
new DescribedPredicate<JavaClass>("定义在club.throwable.controller包下的所有类") {
@Override
public boolean apply(JavaClass input) {
return null != input.getPackageName() && input.getPackageName().contains("club.throwable.controller");
}
};
ArchCondition<JavaClass> condition1 = new ArchCondition<JavaClass>("类名称以Controller结尾") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
String name = javaClass.getName();
if (!name.endsWith("Controller")) {
conditionEvents.add(SimpleConditionEvent.violated(javaClass, String.format("当前控制器类[%s]命名不以\"Controller\"结尾", name)));
}
}
};
ArchCondition<JavaClass> condition2 = new ArchCondition<JavaClass>("方法的入参类型命名以\"Request\"结尾,返回参数命名以\"Response\"结尾") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
Set<JavaMethod> javaMethods = javaClass.getMethods();
String className = javaClass.getName();
// 其实这里要做严谨一点需要考虑是否使用了泛型参数,这里暂时简化了
for (JavaMethod javaMethod : javaMethods) {
Method method = javaMethod.reflect();
Class<?>[] parameterTypes = method.getParameterTypes();
for (Class parameterType : parameterTypes) {
if (!parameterType.getName().endsWith("Request")) {
conditionEvents.add(SimpleConditionEvent.violated(method,
String.format("当前控制器类[%s]的[%s]方法入参不以\"Request\"结尾", className, method.getName())));
}
}
Class<?> returnType = method.getReturnType();
if (!returnType.getName().endsWith("Response")) {
conditionEvents.add(SimpleConditionEvent.violated(method,
String.format("当前控制器类[%s]的[%s]方法返回参数不以\"Response\"结尾", className, method.getName())));
}
}
}
};
ArchRuleDefinition.classes()
.that(predicate)
.should(condition1)
.andShould(condition2)
.because("定义在controller包下的Controller类的类名称以\"Controller\"结尾,方法的入参类型命名以\"Request\"结尾,返回参数命名以\"Response\"结尾")
.check(classes);
}
}
因为导入了所有需要的编译好的类的静态属性,基本上是可以编写所有能够想出来的规约,更多的内容或者实现可以自行摸索。
小结
通过最近的一个项目引入了Archunit,并且进行了一些编码规范和架构规范的规约,起到了十分明显的效果。之前口头或者书面文档的规范可以通过单元测试直接控制,项目构建的时候强制必须执行单元测试,只有所有单测通过才能构建和打包(禁止使用-Dmaven.test.skip=true
参数),起到了十分明显的成效。
参考资料:
个人博客
(e-a-2019216 c-1-d)
项目架构级别规约框架Archunit调研的更多相关文章
- 01-信贷路由项目架构和 rose 框架的搭建
1.信贷路由项目架构 2.工程搭建及测试 搭建tyrRouter-parent,tyrRouter-log-web,工程采用 maven 构建 配置 pom.xml 文件,父项目管理 jar 包的版本 ...
- flask框架实战项目架构
一.项目架构: 研习了多天flask,今天终于按照标准流程写了一个实验demo,并实现了ORM调用,一起喜欢自己写原生SQL.废话不多说,来看项目文件结构 mysite/ ./config/ defa ...
- Angular2.0的项目架构
Angular2.0的项目架构 一.项目服务端app a) Controller控制器 b) Router路由 c) Service服务 d) Public公共样式及脚本和图片等静态资源 e) Vie ...
- 微服务架构的基础框架选择:Spring Cloud还是Dubbo?
最近一段时间不论互联网还是传统行业,凡是涉及信息技术范畴的圈子几乎都在讨论微服务架构.近期也看到各大技术社区开始组织一些沙龙和论坛来分享Spring Cloud的相关实施经验,这对于最近正在整理Spr ...
- Vue 项目架构设计与工程化实践
来源 文中会讲述我从0~1搭建一个前后端分离的vue项目详细过程 Feature: 一套很实用的架构设计 通过 cli 工具生成新项目 通过 cli 工具初始化配置文件 编译源码与自动上传CDN Mo ...
- Springboot项目架构设计
导航 前言 流水线 架构的艺术 项目架构 理解阿里应用分层架构 superblog项目架构 结语 参考 本节是<Spring Boot 实战纪实>的第7篇,感谢您的阅读,预计阅读时长3mi ...
- .Net Core MVC 网站开发(Ninesky) 2.3、项目架构调整-控制反转和依赖注入的使用
再次调整项目架构是因为和群友dezhou的一次聊天,我原来的想法是项目尽量做简单点别搞太复杂了,仅使用了DbContext的注入,其他的也没有写接口耦合度很高.和dezhou聊过之后我仔细考虑了一下, ...
- c#项目架构搭建经验
读过.Net项目中感觉代码写的不错(备注1)有:bbsMax(可惜唧唧喳喳鸟像消失了一样),Umbraco(国外开源的cms项目),Kooboo(国内做开源cms).本人狭隘,读的代码不多,范围也不广 ...
- PHP项目感悟 -- 从CI框架来看iOS的MVC
其实这几天一直都想找时间把这个感悟整理出来,也是这一段一直思考的问题,因为这一段参加一个PHP后台项目的开发,框架使用的是CI,随着项目的进展,对于CI接触的也越多,但是由于理解的可能并不深刻,我也只 ...
随机推荐
- Linux常用命令大全(一)
Linux常用命令大全(一) 第一章 cal命令 $ cal 12 2017 :列出2017年12月的日历 $ cal 10 :列出公元10年的日历 $ cal 12 17 :列出公元17年12月的日 ...
- 洛谷P1037 产生数 题解 搜索
题目链接:https://www.luogu.com.cn/problem/P1037 题目描述 给出一个整数 \(n(n<10^{30})\) 和 \(k\) 个变换规则 \((k \le 1 ...
- rsync 服务介绍及相关实验
一.rsync 简介 rsync 是类 unix 系统下的数据镜像备份工具.一款支持快速完全备份和增量备份的工具,支持本 地复制,远程同步等,类似于 scp 命令:rsync 命令在同步文件之前要先登 ...
- js 鼠标位置
1.clientX.clientY 光标位置——>距离当前body可视区域的x,y坐标 2.pageX.pageY 光标位置——> 对于整个页面来说,包括了被卷去的body ...
- Spring Boot2 系列教程 (五) | yaml 配置文件详解
自定义属性加载 首先构建 SpringBoot 项目,不会的看这篇旧文 使用 IDEA 构建 Spring Boot 工程. 首先在项目根目录 src >> resource >&g ...
- vnpy源码阅读学习(4):自己写一个类似vnpy的UI框架
自己写一个类似vnpy的界面框架 概述 通过之前3次对vnpy的界面代码的研究,我们去模仿做一个vn.py的大框架.巩固一下PyQt5的学习. 这部分的代码相对来说没有难度和深度,基本上就是把PyQt ...
- 保存数据到txt
join用的不错 a = "Hello, world" b = "你好,世界" c = "How are you?" with open(f ...
- 保存数据到csv
csv 逗号分隔值 一.写入 1.列表 单行添加 import csv # with open(file='a.csv', mode='w', encoding='utf-8', newline='' ...
- Vue 组件 传值
注意 Vue模板只能有一个对象,要想用多个对象时用div包裹 一.父组件->子组件 通过props 1.子组件: 声明:proprs =[‘xx’],xx是在父组件中引用子组件,子组件的属性(t ...
- Java入门 - 语言基础 - 03.基础语法
原文地址:http://www.work100.net/training/java-basic-syntax.html 更多教程:光束云 - 免费课程 基础语法 序号 文内章节 视频 1 第一个Jav ...