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

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

背景



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

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


  • 硬性规定项目包结构中service层不能引用controller层的类(这个例子有点极端)。
  • 硬性规定定义在controller包下的Controller类的类名称以"Controller"结尾,方法的入参类型命名以"Request"结尾,返回参数命名以"Response"结尾。
  • 枚举类型必须放在common.constant包下,以类名称Enum结尾。


还有很多其他可能需要定制的规范,最终可能会输出一个文档。但是,谁能保证所有参数开发的人员都会按照文档的规范进行开发?为了保证规范的实行,Archunit以单元测试的形式通过扫描类路径(甚至Jar)包下的所有类,通过单元测试的形式对各个规范进行代码编写,如果项目代码中有违背对应的单测规范,那么单元测试将会不通过,这样就可以从CI/CD层面彻底把控项项目架构和编码规范。本文的编写日期是2019-02-16,当时Archunit的最新版本为0.9.3,使用JDK 8


简介



Archunit是一个免费、简单、可扩展的类库,用于检查Java代码的体系结构。提供检查包和类的依赖关系、调用层次和切面的依赖关系、循环依赖检查等其他功能。它通过导入所有类的代码结构,基于Java字节码分析实现这一点。Archunit的主要关注点是使用任何普通的Java单元测试框架自动测试代码体系结构和编码规则


引入依赖



一般来说,目前常用的测试框架是Junit4,需要引入Junit4Archunit


<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>
复制代码


项目依赖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));
    }
}
复制代码


得到的JavaClassesJavaClass的集合,可以简单类比为反射中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或者通用的内置规则使用,可以参考官方例子


基本使用例子



基本使用例子,主要从一些常见的编码规范或者项目规范编写规则对项目所有类进行检查。


包依赖关系检查


微信截图_20220512194041.png


ArchRule archRule = ArchRuleDefinition.noClasses()
    .that().resideInAPackage("..com.source..")
    .should().dependOnClassesThat().resideInAPackage("..com.target..");
复制代码


微信截图_20220512194054.png


ArchRule archRule = ArchRuleDefinition.classes()
    .that().resideInAPackage("..com.foo..")
    .should().onlyAccessClassesThat().resideInAnyPackage("..com.source..", "..com.foo..");
复制代码


类依赖关系检查


微信截图_20220512194102.png


ArchRule archRule = ArchRuleDefinition.classes()
    .that().haveNameMatching(".*Bar")
    .should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar");
复制代码


类包含于包的关系检查


微信截图_20220512194111.png


ArchRule archRule = ArchRuleDefinition.classes()
    .that().haveSimpleNameStartingWith("Foo")
    .should().resideInAPackage("com.foo");
复制代码


继承关系检查


微信截图_20220512194118.png


ArchRule archRule = ArchRuleDefinition.classes()
    .that().implement(Collection.class)
    .should().haveSimpleNameEndingWith("Connection");
复制代码


微信截图_20220512194126.png


ArchRule archRule = ArchRuleDefinition.classes()
    .that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byAnyPackage("..persistence..");
复制代码


注解检查


微信截图_20220512194133.png


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.moduleonecom.myapp.moduletwocom.myapp.modulethree三个包路径中的类不能形成一个循环依赖缓,例如:


ClassOneInModuleOne -> ClassOneInModuleTwo -> ClassOneInModuleThree -> ClassOneInModuleOne
复制代码


编写规则如下:


slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
复制代码


核心API



把API分为三层,最重要的是"Core"层、"Lang"层和"Library"层。


Core层API


ArchUnit的Core层API大部分类似于Java原生反射API,例如JavaMethodJavaField对应于原生反射中的MethodField,它们提供了诸如getName()getMethods()getType()getParameters()等方法。


此外ArchUnit扩展一些API用于描述依赖代码之间关系,例如JavaMethodCallJavaConstructorCallJavaFieldAccess。还提供了例如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参数),起到了十分明显的成效。


参考资料:


相关文章
|
2月前
|
机器学习/深度学习 安全 算法
十大主流联邦学习框架:技术特性、架构分析与对比研究
联邦学习(FL)是保障数据隐私的分布式模型训练关键技术。业界开发了多种开源和商业框架,如TensorFlow Federated、PySyft、NVFlare、FATE、Flower等,支持模型训练、数据安全、通信协议等功能。这些框架在灵活性、易用性、安全性和扩展性方面各有特色,适用于不同应用场景。选择合适的框架需综合考虑开源与商业、数据分区支持、安全性、易用性和技术生态集成等因素。联邦学习已在医疗、金融等领域广泛应用,选择适配具体需求的框架对实现最优模型性能至关重要。
488 79
十大主流联邦学习框架:技术特性、架构分析与对比研究
|
4月前
|
前端开发 JavaScript 测试技术
Kotlin教程笔记 - 适合构建中大型项目的架构模式全面对比
Kotlin教程笔记 - 适合构建中大型项目的架构模式全面对比
66 3
|
1月前
|
人工智能 JavaScript 安全
【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
101 13
【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
|
13天前
|
人工智能 自然语言处理 并行计算
MeteoRA:多任务AI框架革新!动态切换+MoE架构,推理效率提升200%
MeteoRA 是南京大学推出的多任务嵌入框架,基于 LoRA 和 MoE 架构,支持动态任务切换与高效推理。
47 3
|
4月前
|
存储 分布式计算 关系型数据库
架构/技术框架调研
本文介绍了微服务间事务处理、调用、大数据处理、分库分表、大文本存储及数据缓存的最优解决方案。重点讨论了Seata、Dubbo、Hadoop生态系统、MyCat、ShardingSphere、对象存储服务和Redis等技术,提供了详细的原理、应用场景和优缺点分析。
|
7天前
|
监控 安全 Cloud Native
企业网络架构安全持续增强框架
企业网络架构安全评估与防护体系构建需采用分层防御、动态适应、主动治理的方法。通过系统化的实施框架,涵盖分层安全架构(核心、基础、边界、终端、治理层)和动态安全能力集成(持续监控、自动化响应、自适应防护)。关键步骤包括系统性风险评估、零信任网络重构、纵深防御技术选型及云原生安全集成。最终形成韧性安全架构,实现从被动防御到主动免疫的转变,确保安全投入与业务创新的平衡。
|
5月前
|
人工智能 前端开发 JavaScript
前端架构思考 :专注于多框架的并存可能并不是唯一的方向 — 探讨大模型时代前端的分层式微前端架构
随着前端技术的发展,微前端架构成为应对复杂大型应用的流行方案,允许多个团队使用不同技术栈并将其模块化集成。然而,这种设计在高交互性需求的应用中存在局限,如音视频处理、AI集成等。本文探讨了传统微前端架构的不足,并提出了一种新的分层式微前端架构,通过展示层与业务层的分离及基于功能的横向拆分,以更好地适应现代前端需求。
114 0
|
2月前
|
开发框架 前端开发 .NET
一个适用于 .NET 的开源整洁架构项目模板
一个适用于 .NET 的开源整洁架构项目模板
67 26
|
4月前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
306 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
4月前
|
监控
SMoA: 基于稀疏混合架构的大语言模型协同优化框架
通过引入稀疏化和角色多样性,SMoA为大语言模型多代理系统的发展开辟了新的方向。
169 6
SMoA: 基于稀疏混合架构的大语言模型协同优化框架

热门文章

最新文章