一、什么是checkstyle
在项目开发的过程中,代码规范是经常被提起的话题,特别是当项目需要多个开发协同完成的时候,良好统一的代码规范能够在一定程度上保证项目代码的质量和团队的开发效率。目前业界常见代码检查工具有 Alibaba Java Coding Guidelines,Sonar,checkstyle 等,前两者更多是道德层面的约束,需要依靠自觉自检,而 checkstyle 则可以强制统一开发团队的代码风格和规范。
毕竟在团队开发过程中,每个人都有自己的代码风格,统一代码风格有助于提高代码的可维护性和可读性,也可以减少在 CodeReview 过程中的争议。
二、主要用途
Checkstyle 可以通过定义一系列规则和规范来检查代码,例如缩进、命名约定、代码注释、代码布局等。通过强制执行这些规则,可以确保整个团队编写的代码具有一致的风格,使代码更易于阅读、理解和维护。如果只是把 checkstyle 当作代码风格检查工具,那就有点委屈了它。除了代码风格的统一外,还可以自动化代码审查,可以自动检测代码中的潜在问题和错误,如未使用的变量、未处理的异常、错误的代码格式等,以便在开发阶段变能发现潜在的问题。
同时,checkstyle 能提高代码质量,通过强制执行代码规范和规则,促使开发人员编写更高质量的代码。它可以防止一些常见的编码错误,提高代码一致性和可读性,减少代码维护的成本。同时 checkstyle 提供了灵活的配置选项,允许根据项目的需求自定义规则集,也许一开始使用的时候可能会不习惯,但是用了一段时间回头看整体的代码,就会有赏心悦目的感觉,仿佛在看一件艺术品。
三、插件集成
我们的项目采用的是 Gradle 进行构建,所以本文介绍 Gradle 下 checkstyle 插件的集成,整体集成并不复杂,Maven 也是类似的操作,操作步骤也可以参考 Gradle官网。
在 Gradle 官网上可以看到关于 checkstyle 推荐的目录结构如下:
在根目录下新建 config 目录用来存放 checkstyle 的文件,这里需要准备两个文件,分别是 checkstyle.xml 和 suppressions.xml。前者是用来配置代码检查的规则,后者可以配置忽略检查的一些文件。配置文件可以直接使用谷歌的校验规则 checkstyle.xml,但是整体规则比较严格,这里提供自定义的校验规则模版,可以作为参考:
checkstyle.xml:
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="error"/>
<property name="fileExtensions" value="java, properties"/>
<module name="SuppressionFilter">
<property name="file" value="config/checkstyle/suppressions.xml"/>
</module>
<module name="Header">
<!-- 指定要处理的文件的文件类型扩展名为java -->
<property name="fileExtensions" value="java"/>
</module>
<!--To configure the check to report on the first instance in each file-->
<!--<module name="FileTabCharacter"/>-->
<!-- 文件长度不超过1500行 -->
<module name="FileLength">
<property name="max" value="1500"/>
</module>
<!-- 每个java文件一个语法树 -->
<module name="TreeWalker">
<!-- import检查-->
<!-- 避免使用* -->
<module name="AvoidStarImport"/>
<!-- 检查是否从非法的包中导入了类 -->
<module name="IllegalImport"/>
<!-- 检查是否导入了多余的包 -->
<module name="RedundantImport"/>
<!-- 没用的import检查,比如:1.没有被用到2.重复的3.import java.lang的4.import 与该类在同一个package的 -->
<module name="UnusedImports" />
<!-- 注释检查 -->
<!-- 检查构造函数的javadoc -->
<module name="JavadocType">
<property name="allowUnknownTags" value="true"/>
<message key="javadoc.missing" value="类注释:缺少Javadoc注释。"/>
</module>
<!-- 命名检查 -->
<!-- 包名的检查(只允许小写字母),默认^[a-z]+(\.[a-zA-Z_][a-zA-Z_0-9_]*)*$ -->
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern" value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 仅仅是static型的变量(不包括static final型)的检查 -->
<module name="StaticVariableName" />
<!-- Class或Interface名检查,默认^[A-Z][a-zA-Z0-9]*$-->
<module name="TypeName">
<message key="name.invalidPattern" value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 检查接口类型参数名是否符合指定的模式,默认"^[A-Z]$" -->
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 检查类类型参数名是否符合指定的模式,默认"^[A-Z]$" -->
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 检查方法类型参数名称是否符合指定的模式,默认"^[A-Z]$"-->
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 非static型变量的检查 -->
<module name="MemberName">
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern" value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 方法名的检查 -->
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern" value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 方法的参数名 -->
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- 常量名的检查(只允许大写),默认^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$ -->
<module name="ConstantName" />
<!-- 局部的final变量,包括catch中的参数的检查 -->
<module name="LocalFinalVariableName" />
<!-- 局部的非final型的变量,包括catch中的参数的检查 -->
<module name="LocalVariableName" />
<!-- 检查catch参数名是否符合指定的模式,默认"^(e|t|ex|[a-z][a-z][a-zA-Z]+)$" -->
<module name="CatchParameterName"/>
<!-- 验证标识符名称中的缩写(连续大写字母)长度。-->
<module name="AbbreviationAsWordInName">
<!-- 指出目标标识符(类、接口、变量和方法名称中的缩写,等等)中允许连续大写字母的数量。 -->
<property name="allowedAbbreviationLength" value="6"/>
</module>
<!-- 定义检查 -->
<!-- 检查数组类型定义的样式 -->
<module name="ArrayTypeStyle"/>
<!-- 检查long型定义是否有大写的“L” -->
<module name="UpperEll"/>
<!-- 长度检查 -->
<!-- 检查长方法和构造函数,默认最大150 -->
<module name="MethodLength">
<property name="tokens" value="METHOD_DEF"/>
<property name="max" value="150"/>
<property name="countEmpty" value="false"/>
</module>
<!-- 方法的参数个数。 并且不对构造方法进行检查-->
<module name="ParameterNumber">
<property name="max" value="8" />
<property name="ignoreOverriddenMethods" value="true"/>
<property name="tokens" value="METHOD_DEF" />
</module>
<!-- 空格检查-->
<!-- 检查初始化式的空白填充。默认非空 -->
<module name="EmptyForInitializerPad"/>
<!-- 检查迭代器的空填充,默认非空 -->
<module name="EmptyForIteratorPad"/>
<!-- 方法名后跟左圆括号"(" -->
<module name="MethodParamPad" />
<!-- 在类型转换时,不允许左圆括号右边有空格,也不允许与右圆括号左边有空格 -->
<module name="TypecastParenPad" />
<!-- 检查通用标记周围的空格(尖括号)"<"和">"是正确的典型约定。该约定是不可配置的。 -->
<module name="GenericWhitespace"/>
<!-- 检查在某个特定关键字之后应保留空格 -->
<module name="NoWhitespaceAfter"/>
<!-- 检查在某个特定关键字之前应保留空格 -->
<module name="NoWhitespaceBefore"/>
<!-- 操作符换行策略检查 -->
<module name="OperatorWrap"/>
<!-- 圆括号空白 -->
<module name="ParenPad"/>
<!-- 检查分隔符是否在空白之后 -->
<module name="WhitespaceAfter"/>
<!-- 检查分隔符周围是否有空白 -->
<module name="WhitespaceAround"/>
<!-- 检查非空白字符之间的空格不能超过一个。 -->
<module name="SingleSpaceSeparator"/>
<!-- 检查头、包、所有导入声明、字段、构造函数、方法、嵌套类、静态初始化器和实例初始化器之后的空行分隔符。 -->
<module name="EmptyLineSeparator">
<property name="tokens" value="VARIABLE_DEF, METHOD_DEF"/>
</module>
<!-- 修饰符检查 -->
<!-- 检查修饰符的顺序是否遵照java语言规范,默认public、protected、private、abstract、static、final、transient、volatile、synchronized、native、strictfp -->
<module name="ModifierOrder"/>
<!-- 检查接口和annotation中是否有多余修饰符,如接口方法不必使用public -->
<module name="RedundantModifier"/>
<!-- 代码块检查 -->
<!-- 检查是否有嵌套代码块 -->
<module name="AvoidNestedBlocks"/>
<!-- 检查是否有空代码块 -->
<module name="EmptyBlock"/>
<!-- 检查空的catch块。默认情况下,check允许包含任何注释的空catch块。 -->
<module name="EmptyCatchBlock">
<!-- 如果需要异常的变量名,或者忽略,或者有任何注释,则配置检查来抑制空catch块-->
<property name="exceptionVariableName" value="expected|ignore"/>
</module>
<!-- 检查左大括号位置 -->
<module name="LeftCurly"/>
<!-- 检查代码块是否缺失{} -->
<module name="NeedBraces"/>
<!-- 检查右大括号位置 -->
<module name="RightCurly"/>
<!-- 代码检查 -->
<!-- 检查default是否在switch语句中的所有case之后。 -->
<module name="DefaultComesLast"/>
<!-- 检查空的代码段 -->
<module name="EmptyStatement"/>
<!-- 检查字符串字面值的任何组合是否位于equals()比较的左侧。还检查分配给某些字段的字符串字面量(例如someString。= (anotherString =“文本”))。 -->
<module name="EqualsAvoidNull"/>
<!-- 检查在重写了equals方法后是否重写了hashCode方法 -->
<module name="EqualsHashCode"/>
<!-- 检查switch代码的case中是否缺少break,return,throw和continue。 -->
<module name="FallThrough"/>
<!-- 检查局部变量或参数是否隐藏了类中的变量 -->
<module name="HiddenField">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="ignoreSetter" value="true" />
<property name="ignoreConstructorParameter" value="true" />
</module>
<!-- 检查某些异常类型不会出现在catch语句中。 -->
<module name="IllegalCatch">
<!-- 指定要拒绝的异常类名。 -->
<property name="illegalClassNames" value="Error,java.lang.Error"/>
</module>
<!-- 检查指定的类型是否声明为要抛出。声明一个方法会引发java.lang.Error或java.lang.RuntimeException几乎是不可接受的。 -->
<!--<module name="IllegalThrows"/>-->
<!-- 检查子表达式中是否有赋值操作 -->
<module name="InnerAssignment"/>
<!-- 检查switch语句是否有default -->
<module name="MissingSwitchDefault"/>
<!-- 检查for循环控制变量是否在for块中被修改。-->
<module name="ModifiedControlVariable"/>
<!-- 检查是否有过度复杂的布尔表达式 -->
<module name="SimplifyBooleanExpression"/>
<!-- 检查是否有过于复杂的布尔返回代码段 -->
<module name="SimplifyBooleanReturn"/>
<!-- 类设计检查 -->
<!-- 检查类是否为扩展设计l -->
<!-- 检查只有private构造函数的类是否声明为final -->
<module name="FinalClass"/>
<!-- 检查每个顶级类、接口、枚举或注释是否驻留在自己的源文件中。-->
<module name="OneTopLevelClass"/>
<!-- 检查接口是否仅定义类型 -->
<module name="InterfaceIsType"/>
<!-- 检查类成员的可见度 检查类成员的可见性。只有static final 成员是public的
除非在本检查的protectedAllowed和packagedAllowed属性中进行了设置-->
<module name="VisibilityModifier">
<property name="packageAllowed" value="true"/>
<property name="protectedAllowed" value="true"/>
</module>
<!-- 语法 -->
<!-- String的比较不能用!= 和 == -->
<module name="StringLiteralEquality"/>
<!-- 限制for循环最多嵌套3层 -->
<module name="NestedForDepth">
<property name="max" value="3"/>
</module>
<!-- if最多嵌套4层 -->
<module name="NestedIfDepth">
<property name="max" value="4"/>
</module>
<!-- 检查未被注释的main方法,排除以Appllication结尾命名的类 -->
<!--<module name="UncommentedMain">
<property name="excludedClasses" value=".*[Application,Test,Server]$"/>
</module>-->
<!-- 禁止使用System.out.println -->
<module name="Regexp">
<property name="format" value="System\.out\.println"/>
<property name="illegalPattern" value="true"/>
</module>
<!--try catch 异常处理数量 3-->
<module name="NestedTryDepth ">
<property name="max" value="3"/>
</module>
<!-- 检查没有带零个参数的方法finalize。 -->
<module name="NoFinalizer"/>
<!-- clone方法必须调用了super.clone() -->
<module name="SuperClone" />
<!-- finalize 必须调用了super.finalize() -->
<module name="SuperFinalize" />
<!-- 检查每行是否只有一条语句。-->
<module name="OneStatementPerLine"/>
<!-- 检查是否将重载方法分组在一起。重载的方法具有相同的名称但不同的签名,其中签名可以因输入参数的数量或输入参数的类型或两者的数量而不同。-->
<module name="OverloadMethodsDeclarationOrder"/>
<!-- 确保类有包声明,以及(可选的)包名是否与源文件的目录名匹配。-->
<module name="PackageDeclaration"/>
<!-- 禁止给参数赋值。-->
<module name="ParameterAssignment"/>
<!-- 检查语句或表达式中是否使用了不必要的括号。-->
<!--<module name="UnnecessaryParentheses"/>-->
<!--1.Annotation检查:AnnotationLocation:注解规范, AnnotationOnSameLine:注解同行规范-->
<module name="AnnotationLocation">
<!--注解应该独占一行-->
<property name="allowSamelineMultipleAnnotations" value="false"/>
<property name="allowSamelineSingleParameterlessAnnotation" value="false"/>
<property name="allowSamelineParameterizedAnnotation" value="false"/>
<!--检查对象:类、接口、枚举、方法、构造器、变量-->
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="AnnotationOnSameLine">
<!--检查对象:参数-->
<property name="tokens" value="PARAMETER_DEF" />
</module>
</module>
</module>
suppressions.xml 可以配置忽略检查的文件,比如忽略未使用 jar 包导入的校验规则,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN"
"http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
<suppressions>
<!-- Suppresses the "Unused import" warning for all files in the project -->
<suppress files=".*" checks="UnusedImport" />
</suppressions>
除此之外,还需要在 build.gradle 中引入checkstyle 的插件,因为我们在使用 Gradle 通常都是多模块构建,所以统一放在 allprojects 配置当中,作为子项目的通用配置,该插件默认就会找 config/checkstyle(目录可修改) 下面的配置文件。
具体代码如下:
allprojects {
apply plugin: 'java-library'
apply plugin: 'idea'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'maven-publish'
apply plugin: 'checkstyle'
// JVM 版本号要求
sourceCompatibility = 1.8
targetCompatibility = 1.8
// java编译的时候缺省状态下会因为中文字符而失败
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
// 代码规范检查
checkstyle {
// The version of the code quality tool to be used.
toolVersion = "8.8"
// Whether or not to allow the build to continue if there are warnings.
ignoreFailures = false
// Whether or not rule violations are to be displayed on the console.
showViolations = true
}
}
其中定义了 checkstyle 的 task,声明了对应插件的版本,以及异常情况下是否继续构建和是否在构建控制台显示违反的规则。
四、测试
至此,Gradle 项目就已经集成好了 checkstyle 插件,接下来是测试。简单编写了一个测试代码,其中包含了几点不规范的地方。
执行 gradle build 时候,可以看到控制台生成构建任务 checkstyleMain 代码风格检查时候执行异常,阻止了 Gradle 构建的执行,同时会将检查结果生成一个 HTML 文件。
打开 HTML 文件,可以看到对应那个类存在的问题和触发的规则,以方便对代码修改,符合规范的代码才能通过构建流程。
另外,也可以配置在 git 本地提交的时候自动触发构建,符合代码规范才允许提交。
五、总结
checkstyle 能一定程度上约束和统一代码的风格,对于新项目强烈推荐使用,但是对于老的项目,有一定的历史包袱在里面,想要集成适配的话会有一定的开发成本,要看具体项目的投入来评估。但俗话也说得好,规范只是用来约束君子,而防不了盗贼,所以除了工具的协助外,更多其实在日常写代码中需要我们不断去思考和沉淀,怎样能将代码写得更加优雅、易读、可维护和可扩展,而不只是业务代码上的堆砌,在一些关键代码善于运用一些设计模式来简化业务流程,从而提高项目代码可读性和可维护性,但也要避免过度设计。
以上整体就是 Gradle 集成 checkstyle 的流程和个人的一些思考,如有问题,还请指出,也欢迎大家与我一起交流学习。