静态代码分析
1.介绍
一些大的公司都会进行静态代码分析,他们一般会通过一些定制化的工具或者平台来完成此项工作。对于中小团队来说,可以选用下面几种开源的静态代码分析工具
- Checkstyle:可以用于检查代码风格,例如代码的缩进、每行的最大长度、换行等规范问题
- ArchUnit:可以用于检查代码的分层关系,避免不合理的代码依赖关系出现,比如循环引用等
- FindBugs:可以用于检查潜在的缺陷,例如打开的文件没有关闭、潜在的内存泄漏等
- OWASP Dependency-Check:可以用于检查引入的第三方代码包是否有公开的漏洞等
这些工具基本都有IDE插件,相关插件的使用比较简单,不需要过多介绍,如果希望将其集成到构建过程中,则需要使用相应的配置。海外的Java项目一般使用Gradle(一种构建工具,与Maven类似),国内的Java项目则使用Maven较多
注意:我这里用的JDK版本为17,如果你用的JDK8或11需要自己更换版本
2.Checkstyle
介绍
Checkstyle是一款Java静态代码分析工具,可帮助程序员编写符合编码规范的Java代码。它会自动完成检查,能让程序员避免手工做这些琐碎的事情。
Checkstyle自带了Sun公司和谷歌公司的Java代码风格配置文件,我们可基于此定义适合自己团队的代码规范。可以通过IDEA插件、Maven、Gradle等不同的工具和平台来运行Checkstyle,如果有错误提示,Checkstyle会中断构建并提供友好的报告
使用方式
Checkstyle有两种常用的使用方式,分别是通过代码编辑器IDEA和Maven配合使用
IDEA插件
Maven插件
pom配置
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<!-- checkstyle插件 -->
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version>
<!-- checkstyle plugin使用的checkstyle库,可以自定义此库的版本 -->
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.40</version>
</dependency>
</dependencies>
<executions>
<!-- 加入到 maven 的构建生命周期中去 -->
<execution>
<id>checkstyle</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<failOnViolation>true</failOnViolation>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- 这种方式也是可以的 -->
<plugins>
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<configLocation>${maven.multiModuleProjectDirectory}/dev-support/checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<excludes>**/autogen/**/*</excludes>
</configuration>
<executions>
<execution>
<id>validate</id>
<goals>
<goal>check</goal>
</goals>
<phase>validate</phase>
</execution>
</executions>
</plugin>
</plugins>
主动使用
mvn checkstyle:check
自动使用
由于插件被绑定到了Maven的生命周期中,所以执行构建会自动执行Checkstyle检查,一旦没通过检查则中断构建
自定义检查规则
Checkstyle默认的风格可能会与我们日常开发的习惯不相符,直接使用有可能会导致在日常的开发过程中IDE格式化的结果和Checkstyle冲突、默认的参数过于苛刻等问题。虽然可以将Checkstyle的配置文件导入IDE格式化器的相关配置中,但如果有新同事加入,则又需要额外配置。所以,我们一般都会对Checkstyle默认的配置文件做一些修改,将其调整为适合自己团队的工作方式,尽量按照IDE的默认格式化风格来操作
在Maven的pom文件中通过checkstyle.config.location属性可以配置一个XML文件来定制Checkstyle规则,具体参考如下代码:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 自定义的配置文件,相对于 Pom 文件的路径 -->
<checkstyle.config.location>checkstyle/checkstyle.xml</checkstyle.config.location>
</properties>
基于Google Java代码规则文件配置案例:
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<!--违规级别,用于提示给构建工具,如果是 error 级别会让构建失败-->
<property name="severity" value="warning"/>
<!--扫描的文件类型-->
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<!-- 排除 'module-info.java' 模块描述文件 -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<!--定义忽略规则文件位置-->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/config_whitespace.html -->
<!--检查文件空白制表字符-->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<!--检查单行长度,原规则是 100,但是往往不够用,所以会设置长一点-->
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="160"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<!--检查 Java 源代码语法树-->
<module name="TreeWalker">
<!--检查类型和文件名是否匹配,类名和文件名需要对应-->
<module name="OuterTypeFilename"/>
<!--检查不合规的文本,考虑使用特殊转义序列来代替八进制值或 Unicode 值。-->
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<!--避免使用 Unicode 转义-->
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<!--避免在 import 语句中使用 * -->
<module name="AvoidStarImport"/>
<!--每个文件中只允许有一个顶级类-->
<module name="OneTopLevelClass"/>
<!--该类语句不允许换行-->
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<!--检查空块-->
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<!--检查代码块周围的大括号,这些大括号不允许省略-->
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<!--检查代码块的左花括号的位置-->
<module name="LeftCurly">
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<!--检查代码块的右花括号的位置-->
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<!--检查代码块的右花括号的位置,必须单独一行-->
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<!--检查关键字后面的空格-->
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE"/>
</module>
<!--检查关键字是否被空格包围,一般是语句,比如空构造函数-->
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<!--检查每行只有一个语句-->
<module name="OneStatementPerLine"/>
<!--避免变量连续定义和换行定义,每个变量都需要在自己的行中单独定义-->
<module name="MultipleVariableDeclarations"/>
<!--检查数组类型定义的风格-->
<module name="ArrayTypeStyle"/>
<!--检查 switch 必须具有 default 子句-->
<module name="MissingSwitchDefault"/>
<!--检查 switch 语句,case 子句如果有代码,必须使用 break 语句或抛出异常-->
<module name="FallThrough"/>
<!--检查常量是否用大写定义-->
<module name="UpperEll"/>
<!--检查修饰符是否符合顺序-->
<module name="ModifierOrder"/>
<!--检查空行,在必要的地方需要空行-->
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<!--定义一些不允许换行的关键字,比如点、逗号等-->
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<!--检查包名称是否符合规则-->
<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>
<!--检查类型名称是否符合规则-->
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!--检查实例成员变量是否符合规则-->
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member 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>
<!--检查 Lambda 名称是否符合规则-->
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<!--检查 catch 参数名称是否符合规则-->
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<!--检查本地变量名称是否符合规则-->
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<!--检查类类型参数(泛型)名称是否符合规则-->
<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>
<!--检查字段(record 为 Java 新特性)名称是否符合规则-->
<module name="RecordComponentName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Record component name ''{0}'' must match pattern ''{1}''."/>
</module>
<!--检查字段(record 为 Java 新特性)类型名称是否符合规则-->
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!--检查方法类型参数名称是否符合规则-->
<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>
<!--检查接口类型参数名称是否符合规则-->
<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>
<!--不允许定义无参的 finalize 方法-->
<module name="NoFinalizer"/>
<!--检查尖括号的空白字符规则-->
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<!--检查缩进规则-->
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="2"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<!--检查是否以大写字母作为缩写的长度-->
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="0"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF"/>
</module>
<!--检查覆写方法在类中的顺序-->
<module name="OverloadMethodsDeclarationOrder"/>
<!--检查变量声明与第一被使用之间的距离-->
<module name="VariableDeclarationUsageDistance"/>
<!--检查 import 语句的顺序-->
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<!--检查方法名称和左括号之间的空格-->
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF"/>
</module>
<!--检查关键字前面的空格-->
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<!--检查括号前后是否需要空格-->
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
RECORD_DEF"/>
</module>
<!--检查运算符换行的规则-->
<module name="OperatorWrap">
<!-- 操作符需要在新行-->
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<!--检查注解位置规则,比如类的定义中注释需要单独一行-->
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<!--检查注解位置规则,变量定义注释可以一行定义多个-->
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<!--这部分是注释相关的配置-->
<!--块注释中 @ 子句后面不能为空-->
<module name="NonEmptyAtclauseDescription"/>
<!--检查注释位置,块注释必须在所有注解前面-->
<module name="InvalidJavadocPosition"/>
<!--检查注释必须统一缩进-->
<module name="JavadocTagContinuationIndentation"/>
<!--检查描述性注释,方法的块注释第一行必须总结这个方法,一般我们不要求,会关闭此行-->
<!-- <module name="SummaryJavadoc">-->
<!-- <property name="forbiddenSummaryFragments"-->
<!-- value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>-->
<!-- </module>-->
<!--检查注释段落,段落之间需要换行,另外使用了 <p> 标签不能有空格-->
<module name="JavadocParagraph"/>
<!--检查注释段落,块标签之前需要一个空格,比如 @return -->
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<!--检查注释段落块标签顺序 -->
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<!--检查 public 方法的注释规则 -->
<module name="JavadocMethod">
<property name="scope" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
</module>
<!--对于一些方法可以忽略方法的注释规则。例如,带有Override 注解的方法-->
<module name="MissingJavadocMethod">
<property name="scope" value="public"/>
<property name="minLineCount" value="2"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<!--检查方法必须提供注释的规则 -->
<module name="MissingJavadocType">
<property name="scope" value="protected"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
RECORD_DEF, ANNOTATION_DEF"/>
<property name="excludeScope" value="nothing"/>
</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="SingleLineJavadoc"/>
<!--检查空的 catch 块-->
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<!--检查注释代码之间的缩进-->
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml" />
<property name="optional" value="true"/>
</module>
</module>
</module>
3.FindBugs
介绍
FindBugs是一个开源工具,可用于对Java代码执行静态代码分析,其由马里兰大学Bill Pugh领导的团队研发,实现原理是对字节码进行扫描并进行模式识别。和Checkstyle不一样的是,FindBugs会通过对代码的模式进行分析来发现潜在的Bug和安全问题,而Checkstyle只能作为检查代码风格的工具。虽然FindBugs和Checkstyle的部分功能重叠,但两者的定位明显不同
注意
FindBugs已不再更新,且无法兼容Java9+
问题类型
FindBugs中包含下面几种问题类型
- Correctness:由开发人员疏忽造成的正确性问题,比如无限递归调用
- Bad practice:代码中的一些坏习惯,比如使用==对String做判定
- Dodgy code:糟糕的代码,能工作但不是好的实现,比如冗余的流程控制
- Multithreaded Correctness:多线程和并发问题
- Malicious Code Vulnerability:恶意代码漏洞
- Security:安全问题
- Experimental:经验性问题
- Internationalization:国际化问题
使用方式
FindBugs有两种常用的使用方式,使用IntelliJ IDEA的插件做本地分析,或者作为Maven、Gradle的任务在构建过程中运行
IDEA插件
FindBugs插件在IntelliJ IDEA早期的版本中是独立提供的,后来需要先安装QAPlug这个静态代码分析工具,并作为QAPlug的一个模块提供
QAPlug提供了代码分析和扫描的功能,并且能集成PMD等诸多模块。不过,使用它们需要同时安装QAPlug和FindBugs这两个插件,并且要在安装后重启
通过IDEA首选项的插件市场即可安装QAPlug和FindBugs,如下图所示:
这两个插件的使用方法比较简单,参考下图,直接在需要扫描的目录或者模块上点击右键,就会弹出代码分析菜单
分析完成后,在底部面板中会弹出分析结果,如下图所示:
从上图可以看出,这里扫描出代码中存在一个问题,即在某一个方法中使用了浮点类型做数学运算,存在潜在的精度问题
Maven插件
想要在构建过程中使用FindBugs(如果存在问题可以让构建失败),可以使用Maven的插件来运行
创建Maven项目后,在pom文件的build块中添加Maven插件即可开启FindBugs功能:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<version>3.0.5</version>
<configuration>
<effort>Max</effort>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
在configuration属性的配置中,effort参数比较常用,其含义是使用不同程度的算力进行分析。Effort参数有max和min这两个值,使用max意味着需要花费更多的内存和时间来找出更多的缺陷;使用min则会关闭一些需要花费更多时间和内存的分析项。如果发现运行过程中耗时严重,可以调整这个值
其他参数及其配置方式可以参考FindBugs和FindBugs Maven插件的相关文档
常见错误
介绍
在示例项目中,可能有读者已经找到了FindBugs模块,这个模块中提供了一些典型的问题,这些问题在日常修复FindBugs时出现的频率较高。即便你不使用FindBugs,也需要了解这些常见的问题模式,虽然这些问题IDE往往也都会提示
精度问题
由于计算机通过二进制无法完全表达某些小数,因此会对精度进行取舍,故而我们在使用小数进行数学运算时需要注意精度问题。示例如下:
private static void mathCalculate() {
double number1 = 0.1;
double number2 = 0.2;
double number3 = 0.3;
if (number1 + number2 == number3) {
System.out.println("精度问题示例");
}
}
无限递归调用
递归程序需要设定基本的结束条件,否则会一直运行下去,直到栈溢出。示例如下:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String name() {
return name();
}
}
Person testPerson = new Person("test");
testPerson.name();
空指针问题
Java是完全面向对象的语言,因此我们在使用对象中的成员时需要注意对象是否存在。示例如下:
private static void nullIssue() {
String test = null;
if (test != null || test.length() > 0) {
System.out.println("空指针异常");
}
if (test == null && test.length() > 0) {
System.out.println("相反的情况,导致空指针异常");
}
}
潜在死锁问题
synchronized是对象排他锁,而字符串的字面量是整个JVM共享的,因此容易造成死锁,我们往往也容易疏忽。示例如下:
private static final String lockField = "LOCK_PLACE_HOLDER";
private static void deadLock() {
synchronized (lockField) {
System.out.println("死锁问题");
}
}
动态的死锁比较难扫描出来,在后面的内容中会专门讨论这个话题
忘记使用throw语句抛出异常
异常被创建后不使用throw语句抛出,编译器并不会报错,但是应该抛出的异常没有被抛出,则可能存在潜在的业务逻辑问题。示例如下:
private static void noThrow() {
boolean condition = false;
if (condition) {
// 忘记 throw 一个异常,仅仅创建了
new RuntimeException("Dissatisfied condition");
}
}
相等判定问题
对象是否相等需要根据具体的逻辑来判断,像基本类型一样简单根据运算符==来进行判定并不可靠。示例如下:
private static void equalsString() {
String sting1 = "test";
String sting2 = "test";
if (sting1 == sting2) {
System.out.println("不安全的相等判定");
}
}
字符串循环拼接
字符串是不可变对象,采用字符串循环拼接方式会导致代码性能低下,示例如下:
private static void stringConcat() {
String sting = "test";
// 应该使用 String Builder
for (int i = 0; i < 1000; i++) {
sting += sting;
}
}
忘记使用返回值
在一些方法中,方法不会对参数本身做修改,因此需要接收返回值实现业务逻辑,这部分往往会出现Bug,示例如下:
private static void forgotReturnValue() {
List<String> list = Arrays.asList("hello");
// map 需要使用返回值
list.stream().map(String::toUpperCase);
String hello = "hello ";
// 字符串操作也需要返回值
hello.trim();
}
数组不使用迭代器删除元素
如果数组不使用迭代器删除元素,而是直接在for循环中删除,那么会触发Concurrent-ModificationException,示例如下:
private static void arrayListRemoveException() {
ArrayList<String> list = new ArrayList<>();
// 直接在 for 循环中删除了元素
for (String item : list) {
list.remove(item);
}
}
资源忘记关闭
Java的垃圾回收器只负责处理内存回收,字节流、网络、文件、进程等相关资源都需要手动关闭。比如下面的字节流:
private static void forgotCloseStream() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream s = null;
// 需要关闭流
try {
s = new ObjectOutputStream(out);
s.writeObject(1);
} catch (IOException e) {
e.printStackTrace();
}
}
数据截断
强制类型转换也会存在潜在的Bug,它会导致数据被截断
private static void objectCastIssue() {
long number = 1000L;
// 数据会被截断
int number2 = (int) number;
}
这些问题都非常常见,通过FindBugs基本都可以找出来,如此一来,即可有效地减少代码评审的压力
4.ArchUnit
介绍
通过Checkstyle解决了代码的风格问题,又使用FindBugs解决了基本的代码质量问题,现在还需要解决开发过程中的架构规范问题
有足够经验的开发者都知道,软件项目和架构极其容易腐化。如果没有很好地管控,无论是采用MVC的三层架构还是DDD的四层架构,代码的结构都会在几个月内变得混乱不堪
事实上,可以让包结构检查成为自动化检查的一部分,从而节省团队技术经理的管理精力。ArchUnit作为一个小型、简单、可扩展的开源Java测试库,可用于验证预定义的应用程序体系结构和约束关系
包结构
介绍
在使用ArchUnit之前,我们需要讨论一下常见的代码划分包结构的方式。因微服务和单体系统下代码的背景不同,故而不同项目的包结构划分策略也会有所不同,这里按照单体系统下的结构来说明
Java应用项目中一般有两种组织代码的方式。一种是按照大平层的风格组织,即将同一类代码放到一个包中,比如Service、Dao;还有一种是按照业务模块来划分,每个模块下有自己的大平层
另外,不同的代码也会有不同的层次划分方式。这里介绍两种,一种是MVC风格的三层结构,即Controller、Service和Dao;另外一种是DDD的四层结构,即Interface、Application、Domain和Infrastructure
MVC大平层分包
这是一种最简单的分包方式,如下图所示,按照最初MVC模式的逻辑,业务应该写在Controller中。但是随着前后端分离的发展,View层消失了。在Spring Boot等框架中,Controller通过RESTful的注解代替了View层,主流的做法演化成将业务逻辑写在Service中
为了保持架构整洁,这种分包结构下需要有如下简单规则:
- 相同类型的文件放到相同的包中
- 上层对象可以依赖下层对象,禁止反向依赖
- Request对象只能在Controller中使用,为了保持Service层的复用性,不允许在Service中引用Controller层的任何类
- 不建议将Model直接用于接口的数据输出,而应该转换为特定的Response类
- 所有文件需要使用包名作为结尾,例如UserController、UserService、UserModel、UserDao等
这是一种最简单、清晰的包结构划分,这里还没有涉及枚举、远程调用、工厂等更为细节的包结构设计,可以继续按照需要拓展
MVC按照模块分包
大平层的分包方式在大多数项目中已经够用,但是对于一些复杂的项目,这种包结构会受到团队的质疑,这是因为业务很复杂时,每一个目录下的文件都会非常多。这时,可根据业务划分模块,每个模块下再设置单独的大平层结构,如下图所示:
在规模较大、复杂的应用中按照模块分包,可以将单个开发者的认知负载降低。虽然按照这种方式分包可以将各个业务模块分开,简化单个模块的开发复杂度,但是会让系统整体变得复杂。我们在享受这种分包好处的同时,需要额外注意它带来的问题。例如用户模块的Controller可以访问商品模块的Service,商品模块的Service又可以转而访问用户模块的Dao,随着时间的流逝,虽然各个模块的文件看起来都是分开的,但是业务依然会混乱
为了解决这个问题,在使用这种分包方式时,除了需要遵守上面的规则以外,还需要额外增加如下规则:
- 跨模块访问时,不允许直接访问Dao,而是应访问对方的Service
- 模块之间应该通过Service互相访问,而不是通过表关联
- 模块之间不允许存在循环依赖,如果产生循环依赖,应该重新设计
DDD大平层分包
MVC分包方式虽然能满足大部分项目的需求,但是对于越来越复杂的规模化应用来说,也有一定的局限性
举个例子,当我们的应用需要支持多个角色的操作时,MVC就会带来一定的混乱。这里的角色不是指管理员和超级管理员那种仅仅是权限不同的角色,而是指管理员、用户、代理商等完全不同的操作逻辑和交互行为。这种思想和DDD的分层思想不谋而合
如下图所示,DDD的四层结构使用了不同的概念
- Interface层:用于隔离接口差异,即比如XML、WebSocket、JSON等
- Application层:用于隔离应用差异,即将用户的操作和管理员的操作区分开
- Domain层:用于复用业务逻辑
- Infrastructure 层:一些基础设施,例如数据库、Redis、远程访问等
可以看到, DDD大平层分包方式划分的包结构和MVC区别不算大,主要是将应用层隔离,而将领域层的同类型代码放到一起,使用规则也类似
DDD基于模块分包
DDD也可以基于模块分包,如下图所示,这里的模块划分只会针对于领域对象和领域服务进行,其中涉及一个专门的术语——上下文
需要注意的是 ,DDD基于模块分包并不是一股脑地将所有的Controller、Service纳入某个模块中,这种做法会造成业务进一步混乱。它是将应用和领域分开,再按照不同的逻辑进行拆分
DDD 基于模块分包时,需要遵守如下规则:
- 应用可依赖于领域,领域不允许依赖于应用
- 上下文之间不允许存在循环依赖
- 上下文之间的访问需要通过Domain Service完成,不能直接调用对方的数据层
以上四种分包的方式虽然有所不同,但是相差得不多。我们应该根据自己的业务情况来选择分包的方式,如果简单的业务使用较为复杂的包结构,会带来非常多的样板代码,降低开发效率。
总结
分层的本质是隔离差异,如果在系统可知的运行时间内没有差异的出现,可以先不考虑分层,这种分层除了人为增加复杂度之外往往没有任何好处
当然,还有洋葱架构、六边形架构等其他架构方式,但是相对小众,这里暂且不做分析
考虑好分包方式后,我们就可以配置ArchUnit检查条件和约束规则了。后文将以MVC大平层分包方式为例,说明如何使用ArchUnit对包结构进行检查。当然,不使用ArchUnit也可以通过团队契约、多模块的项目设计对团队开发做一些约束
使用方式
ArchUnit的使用比较简单,可以通过JUnit的Runner运行,也可以通过Maven、Gradle等构建工具来运行
JUnit使用
介绍
ArchUnit支持不同的测试框架,这里使用的是JUnit 4,ArchUnit更像是进行代码规范的检查而不是测试,虽然它使用了JUnit平台,但其实大家更愿意把它划分到静态代码分析中
添加依赖
在Maven中使用ArchUnit,首先需要添加相关的依赖,命令如下:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
案例准备
在下图中准备了一个Demo应用,它有三个包和三个主要的类
我们可以使用下面的规则编写ArchUnit测试:
- Controller中的类不允许被Service、Dao访问
- 所有的类名必须使用当前的包名结尾
需求一
然后在对应的测试目录下,编写一个测试类ArchUnitTest,并添加一个测试用例来限制类名,所有的Controller文件必须以Controller结束:
public void file_name_should_end_with_package_name() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(this.getClass().getPackage().getName());
classes().that().resideInAPackage("..controller")
.should().haveSimpleNameEndingWith("Controller")
.check(importedClasses);
classes().that().resideInAPackage("..service")
.should().haveSimpleNameEndingWith("Service")
.check(importedClasses);
classes().that().resideInAPackage("..dao")
.should().haveSimpleNameEndingWith("Dao")
.check(importedClasses);
}
在上述代码中,importedClasses为被覆盖的范围,ArchUnit可以通过ClassFileImporter、JavaTypeImporter等方式加载需要被验证的类
上面这段测试中包含了3条验证规则,下面这段代码就是其中一条。使用ArchUnit只需要按照类似的做法编写这些规则即可
classes().that().resideInAPackage("..controller")
.should().haveSimpleNameEndingWith("Controller")
.check(importedClasses);
这是一个典型链式风格的API,classes()方法是ArchUnit lang层的工具方法,用于声明基本的规则,大部分基本规则都可以使用classes() 方法来初始化声明。that()方法后面的内容代表哪些符合规则的类会被筛选到。ArchUnit提供了大量的筛选器,比如类型、是否使用了某种注解等。should()方法后面接的是断言规则,比如类名规则、依赖规则等
需求二
接下来实现MVC分层架构的依赖检查,这里会用到library包中的预定义规则方法layeredArchitecture(),示例代码如下:
@Test
public void should_obey_MVC_architecture_rule() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(this.getClass().getPackage().getName());
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Dao").definedBy("..dao..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Dao").mayOnlyBeAccessedByLayers("Service");
layeredArchitecture.check(importedClasses);
}
执行上述代码时,在IDEA编辑器边缘会出现绿色的运行按钮,单击此按钮即可作为单元测试运行。这里使用layeredArchitecture()将controller、service和dao三个包中的类分别定义为Controller、Service和Dao层,并声明其约束关系。如果出现错误的依赖关系,测试就不会通过
官网使用了一张图来说明三层架构下的依赖关系,可以看到,这里只允许下层类被上层调用,以此来守护代码的架构。在编写本书时,官网的示例代码存在部分未更新的情况,如果按照官网的说明不能运行,可以参考本书提供的示例代码
5.OWASP Dependency-Check
介绍
架构的问题解决后,还需要避免在项目中使用开源软件带来的安全问题。开放式Web应用程序安全项目(OWASP)是一个非营利组织,提供了安全标准、数据库、社区和培训。其中一个工具OWASP Dependency-Check可以对第三方依赖包中的知名漏洞进行检查,扫描结果受漏洞数据库的更新影响
OWASP Dependency-Check可以报告现有的第三方依赖的CVE。CVE的英文全称是Common Vulnerabilities & Exposures,可以简单地理解为业界已知的漏洞批漏
OWASP的安全扫描和fortify等安全扫描工具有所不同,它依赖于开放的漏洞信息,不能完全代替模式分析类安全扫描工具。即便如此,基于OWASP Dependency-Check进行的依赖检查也是必须的,因为现代项目依赖的组件较多,通过人工检查的方式较难及时发现漏洞
使用
OWASP的依赖检查支持主流的语言和包管理工具,对于Java语言来说,我们可以继续使用Maven插件来运行OWASP Dependency-Check。与Checkstyle类似,首先创建一个模块,在pom文件中添加相关依赖:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>6.1.3</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
在真实的项目中,依赖包的变化没有那么频繁,如果每次构建都运行这个检查会让构建变慢。比较好的做法是使用CI/CD工具,比如Jenkins,设定一个定时的任务在夜间运行
使用OWASP Dependency-Check时需要意识到,它并不能取代安全测试。由于它的实现机制是通过CVE库来报告问题的,因此受制于该库的更新情况,存在一定程度上的滞后问题