专栏首页孟君的编程札记ArchUnit, 代码结构规范检查神器,你值得拥有

ArchUnit, 代码结构规范检查神器,你值得拥有

本文将向大家介绍一个代码结构检查的神器 - - ArchUnit。在正式介绍ArchUnit之前,先请大家思考一下:

为什么需要对代码结构进行检查或者测试?

相信大部分的的开发人员有遇到过这样的情况(尤其是在项目逐渐变大的场景下):

开始有人画了一些漂亮的架构图,展示了系统应该包含的组件以及它们应该如何交互,大家形成一个约定并达成共识。但是随着项目逐渐变得更大,一般会经历开发人员的调整,包括新开发人员的加入或者老开发人员离开去做其它项目等。当新的需求或者特性添加进来,由于开发人员的差异,可能会出现一些不可预见的违反规范的行为,如:

  • 命名不规范
  • 分层代码调用不规范,比如Controller直接调用Dao
  • ... ...

这些问题可能需要在代码Review的时候才会被看到,并不是一种很及时的解决方法。

一、ArchUnit简介和入门

1.1 简介

ArchUnit is a free, simple and extensible library for checking the 
architecture of your Java code.
That is, ArchUnit can check dependencies between packages 
and classes, layers and slices, check for cyclic dependencies 
and more. It does so by analyzing given Java bytecode, 
importing all classes into a Java code structure. 
ArchUnit’s main focus is to automatically test architecture 
and coding rules, using any plain Java unit testing framework.

from -- https://www.archunit.org/userguide/html/000_Index.html

从上述ArchUnit的官网描述可以看出,ArchUnit是一个免费、简单和可扩展的库,用于检查Java代码的结构。ArchUnit提供了包和类之间依赖关系、循环依赖等方面的检测。ArchUnit的主要目标是使用纯Java的单元测试框架来达到自动化检测代码结构和编码规则。

1.2 快速开始

如果您想直接进入第一个ArchUnit测试,请按照以下步骤操作。

  • 添加ArchUnit依赖
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>0.11.0</version>
    <scope>test</scope>
</dependency>
  • 创建一个测试类
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

public class MyArchitectureTest {
    @Test
    public void some_architecture_rule() {
        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp");
    
        ArchRule rule = classes()... // see next section
    
        rule.check(importedClasses);
    }
}
  • 根据API提示进行后续操作

如下是一个Hello World的示例,

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

// ...

private final ClassFileImporter importer = new ClassFileImporter();

private JavaClasses classes;

@Before
public void importClasses() {
    classes = importer.importClasspath(); // imports all classes from the classpath that are not from JARs
}

@Test
public void one_should_not_access_two() {
    ArchRule rule = noClasses().that().resideInAPackage("..one..")
        .should().accessClassesThat().resideInAPackage("..two.."); // The '..' represents a wildcard for any number of packages

    rule.check(classes);
}

// ...

如果上述规则违反了,单元测试会失败并报如下错误信息:

二、典型检测示例

2.1 包依赖检测

noClasses().that().resideInAPackage("..source..")
    .should().dependOnClassesThat().resideInAPackage("..foo..")
classes().that().resideInAPackage("..foo..")
    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")

2.2类依赖检测

classes().that().haveNameMatching(".*Bar")
    .should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar")

2.3 类和包的包含关系检测

classes().that().haveSimpleNameStartingWith("Foo")
    .should().resideInAPackage("com.foo")

2.4 继承检测

classes().that().implement(Connection.class)
    .should().haveSimpleNameEndingWith("Connection")
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byAnyPackage("..persistence..")

2.5 注解检测

classes().that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)

2.6 分层检测

layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

2.7 循环检测

slices().matching("com.myapp.(*)..").should().beFreeOfCycles()

三、ArchUnit API组成部分

ArchUnit主要提供的API有Core、Lang和Library等几个部分。

其中,

  • The Core API

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

ArchUnit提供了ClassFileImporter用于导入已经编译好的Java class文件:

JavaClasses classes = new ClassFileImporter()
                            .importPackages("com.mycompany.myapp");
  • The Lang API

Core层的API十分强大,提供了需要关于Java程序静态结构的信息,但是直接使用Core层的API对于单元测试会缺乏表现力,特别表现在架构规则方面。

ArchUnit提供了Lang层的API,它提供了一种强大的语法来以抽象的方式表达规则。Lang层的API大多数是采用流式编程方式定义方法,例如指定包定义和调用关系的规则如下:

ArchRule rule =
    classes().that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
  • The Library API

Library层API通过静态工厂方法提供了更多复杂而强大的预定义规则。

四、更多示例

4.1 创建自定义规则

ArchUnit提供了许多预定义的语法来完成访问字段、访问方法、访问包等,一般的语法结构如下:

classes that ${PREDICATE} should ${CONDITION}

如果不能满足规则,可以通过DescribedPredicate和ArchCondition来完成自定义规则,主要格式如下:

DescribedPredicate<JavaClass> resideInAPackageService = // define the predicate
ArchCondition<JavaClass> accessClassesThatResideInAPackageController = // define the condition

noClasses().that(resideInAPackageService)
    .should(accessClassesThatResideInAPackageController);

一个示例:

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);

如果违反了上述规则,会报如下错误信息:

classes that have a field annotated with @Payload 
should only be accessed by @Secured methods

4.2 忽略某些违规行为

在遗留项目中引入结构检测,可能有太多的违规行为无法一次修复,可以先对一些违规行为进行忽略。一般的做法是定义一个记录忽略规则的文件,如archunit_ignore_patterns.txt,该文件放在根路径中。

# There are many known violations where LegacyService is involved; we'll ignore them all
.*some\.pkg\.LegacyService.*

4.3 高级配置

一些行为可以在统一的配置文件中指定,配置文件必须命名为archunit.properties, 并存放根路径下。支持的配置选项如下所示:

# E.g. if a class calls a method, but the declaring class is not within the scope of the import,
# like in a case, where a package like 'my.app' is imported, and java.lang.String#length is called.
# Should ArchUnit try to locate the missing class on the classpath and import it as well?
#
# default = false - This has a performance impact
resolveMissingDependenciesFromClassPath=true

# Extends the customizability of 'resolveMissingDependenciesFromClassPath' by allowing to specify
# a custom implementation of ClassResolver. Such a custom implementation has full control, how
# type names should be resolved against JavaClasses. SelectedClassResolverFromClasspath is one example,
# it allows to resolve some types from the classpath (based on their package, while others are 
# just stubbed. E.g. if you want to resolve classes from your own app, but not from java.util.. 
# or similar).
#
# classResolver.args allows to configure constructor parameters, to be supplied to a constructor
# accepting a single List<String> parameter. If no arguments are configured, a default constructor
# is supported as well.
#
# default = absent - fall back to evaluating 'resolveMissingDependenciesFromClassPath'
classResolver=com.tngtech.archunit.core.importer.resolvers.SelectedClassResolverFromClasspath
classResolver.args=com.tngtech.archunit.core,com.tngtech.archunit.base

# Should ArchUnit include the MD5 sum of imported classes into the JavaClass#getSource()?
# This way failure tracking can be improved, if there are inconsistencies within the imported sources.
# 
# default = false - This has a performance impact
enableMd5InClassSources=true

五、小结

本文主要对ArchUnit进行了简单的介绍。我们可以通过在项目中引入ArchUnit,对结构完成自动化检测,持续性构建。更多的内容可以到ArchUnit官网https://www.archunit.org/userguide/html/000_Index.html进行了解。

另外可能有读者疑问,为什么要使用ArchUnit呢?这个问题,其官网也给出了解释,这里就不再具体说明了。

本文分享自微信公众号 - 孟君的编程札记(gh_0f0f5e0ae1de),作者:孟君

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一步步完成thrift rpc示例

    本文给出一个在Windows下,使用thrift一步步完成rpc的Java示例。文章主要从如下几个部分来加以说明:

    孟君
  • 一步步完成Maven+SpringMVC+SpringFox+Swagger整合示例

    本文给出一个整合Maven+SpringMVC+SpringFOX+Swagger的示例,并且一步步给出完成步骤。

    孟君
  • 使用Exchanger实现线程间的数据交换

    从JDK 1.5之后,在java.util.concurrent包下引入了好多的处理多线程的工具类,本文介绍Exchanger工具类, 然后采用Exchange...

    孟君
  • 使用Kotlin 1.1.5 的REPL 来简单分析一下Java 9 中的$ jmod list java.base.jmod《Kotlin极简教程》正式上架:

    命令行列出了 模块 java.base.jmod 中所有文件(.class文件, .dat, .jar, .cfg, .dylib 等 )共 5761个文件...

    一个会写诗的程序员
  • rpc框架之 thrift连接池实现

    接前一篇rpc框架之HA/负载均衡构架设计 继续,写了一个简单的thrift 连接池: 先做点准备工作: package yjmyzz; public cla...

    菩提树下的杨过
  • LeetCode题组:第914题-卡牌分组

    每组都有 X 张牌。 组内所有的牌上都写着相同的整数。 仅当你可选的 X >= 2 时返回 true。

    明天依旧可好
  • Redis源码解析——前言

            今天开启Redis源码的阅读之旅。对于一些没有接触过开源代码分析的同学来说,可能这是一件很麻烦的事。但是我总觉得做一件事,不管有多大多难,我们首...

    方亮
  • (1)当你输入URL到页面显示经历了什么--URL到IP地址

    这是一个经典的问题,能区分知识的广度与深度,从回答的侧重点上甚至能区分出工种(前端、后端、运维等)。开发人员基本上都能说出几点,而牛人更可在自己...

    前端黑板报
  • AngularDart Material Design 扩展面板 顶

    一个或多个面板在扩展面板集中组合在一起。 单击面板时,面板内容将展开。 面板由名称,值,可选的辅助文本和展开的面板内容组成。

    南郭先生
  • 一个优秀的Android应用从建项目开始

    1.项目结构 现在的MVP模式越来越流行。就默认采用了。 如果项目比较小的话: app——Application Activity Fragment Prese...

    非著名程序员

扫码关注云+社区

领取腾讯云代金券