前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ArchUnit, 代码结构规范检查神器,你值得拥有

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

作者头像
孟君
发布2019-10-28 17:48:48
3.5K0
发布2019-10-28 17:48:48
举报
文章被收录于专栏:孟君的编程札记

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

代码语言:javascript
复制
为什么需要对代码结构进行检查或者测试?

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

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

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

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

一、ArchUnit简介和入门

1.1 简介

代码语言:javascript
复制
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依赖
代码语言:javascript
复制
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>0.11.0</version>
    <scope>test</scope>
</dependency>
  • 创建一个测试类
代码语言:javascript
复制
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的示例,

代码语言:javascript
复制
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 包依赖检测

代码语言:javascript
复制
noClasses().that().resideInAPackage("..source..")
    .should().dependOnClassesThat().resideInAPackage("..foo..")
代码语言:javascript
复制
classes().that().resideInAPackage("..foo..")
    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")

2.2类依赖检测

代码语言:javascript
复制
classes().that().haveNameMatching(".*Bar")
    .should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar")

2.3 类和包的包含关系检测

代码语言:javascript
复制
classes().that().haveSimpleNameStartingWith("Foo")
    .should().resideInAPackage("com.foo")

2.4 继承检测

代码语言:javascript
复制
classes().that().implement(Connection.class)
    .should().haveSimpleNameEndingWith("Connection")
代码语言:javascript
复制
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byAnyPackage("..persistence..")

2.5 注解检测

代码语言:javascript
复制
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)

2.6 分层检测

代码语言:javascript
复制
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 循环检测

代码语言:javascript
复制
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文件:

代码语言:javascript
复制
JavaClasses classes = new ClassFileImporter()
                            .importPackages("com.mycompany.myapp");
  • The Lang API

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

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

代码语言:javascript
复制
ArchRule rule =
    classes().that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
  • The Library API

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

四、更多示例

4.1 创建自定义规则

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

代码语言:javascript
复制
classes that ${PREDICATE} should ${CONDITION}

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

代码语言:javascript
复制
DescribedPredicate<JavaClass> resideInAPackageService = // define the predicate
ArchCondition<JavaClass> accessClassesThatResideInAPackageController = // define the condition

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

一个示例:

代码语言:javascript
复制
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);

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

代码语言:javascript
复制
classes that have a field annotated with @Payload 
should only be accessed by @Secured methods

4.2 忽略某些违规行为

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

代码语言:javascript
复制
# There are many known violations where LegacyService is involved; we'll ignore them all
.*some\.pkg\.LegacyService.*

4.3 高级配置

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

代码语言:javascript
复制
# 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呢?这个问题,其官网也给出了解释,这里就不再具体说明了。

本文参与?腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-24,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 孟君的编程札记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体同步曝光计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com