01.静态代码分析

18 阅读19分钟

静态代码分析

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插件

0592af6ee9dc58da9b76f8ee32a818fa.png d81dbe5f9d9939c6f3924b4b74391f19.png 27fee4a6629536660a1808a9d9f790f6.png 0284011a3b6bb2336548a47ccd96a0b2.png 30293d1791c204dad5bc52ae8f71830d.png

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,如下图所示:

image11764510825997.png

这两个插件的使用方法比较简单,参考下图,直接在需要扫描的目录或者模块上点击右键,就会弹出代码分析菜单

asdsadscccqc.png

分析完成后,在底部面板中会弹出分析结果,如下图所示:

asdasdadasdaafasdadsa.png

从上图可以看出,这里扫描出代码中存在一个问题,即在某一个方法中使用了浮点类型做数学运算,存在潜在的精度问题

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中

image41764511571119.png

为了保持架构整洁,这种分包结构下需要有如下简单规则:

  • 相同类型的文件放到相同的包中
  • 上层对象可以依赖下层对象,禁止反向依赖
  • Request对象只能在Controller中使用,为了保持Service层的复用性,不允许在Service中引用Controller层的任何类
  • 不建议将Model直接用于接口的数据输出,而应该转换为特定的Response类
  • 所有文件需要使用包名作为结尾,例如UserController、UserService、UserModel、UserDao等

这是一种最简单、清晰的包结构划分,这里还没有涉及枚举、远程调用、工厂等更为细节的包结构设计,可以继续按照需要拓展

MVC按照模块分包

大平层的分包方式在大多数项目中已经够用,但是对于一些复杂的项目,这种包结构会受到团队的质疑,这是因为业务很复杂时,每一个目录下的文件都会非常多。这时,可根据业务划分模块,每个模块下再设置单独的大平层结构,如下图所示:

image51764511678647.png

在规模较大、复杂的应用中按照模块分包,可以将单个开发者的认知负载降低。虽然按照这种方式分包可以将各个业务模块分开,简化单个模块的开发复杂度,但是会让系统整体变得复杂。我们在享受这种分包好处的同时,需要额外注意它带来的问题。例如用户模块的Controller可以访问商品模块的Service,商品模块的Service又可以转而访问用户模块的Dao,随着时间的流逝,虽然各个模块的文件看起来都是分开的,但是业务依然会混乱

为了解决这个问题,在使用这种分包方式时,除了需要遵守上面的规则以外,还需要额外增加如下规则:

  • 跨模块访问时,不允许直接访问Dao,而是应访问对方的Service
  • 模块之间应该通过Service互相访问,而不是通过表关联
  • 模块之间不允许存在循环依赖,如果产生循环依赖,应该重新设计
DDD大平层分包

MVC分包方式虽然能满足大部分项目的需求,但是对于越来越复杂的规模化应用来说,也有一定的局限性

举个例子,当我们的应用需要支持多个角色的操作时,MVC就会带来一定的混乱。这里的角色不是指管理员和超级管理员那种仅仅是权限不同的角色,而是指管理员、用户、代理商等完全不同的操作逻辑和交互行为。这种思想和DDD的分层思想不谋而合

如下图所示,DDD的四层结构使用了不同的概念

  • Interface层:用于隔离接口差异,即比如XML、WebSocket、JSON等
  • Application层:用于隔离应用差异,即将用户的操作和管理员的操作区分开
  • Domain层:用于复用业务逻辑
  • Infrastructure 层:一些基础设施,例如数据库、Redis、远程访问等

可以看到, DDD大平层分包方式划分的包结构和MVC区别不算大,主要是将应用层隔离,而将领域层的同类型代码放到一起,使用规则也类似

image6.png

DDD基于模块分包

DDD也可以基于模块分包,如下图所示,这里的模块划分只会针对于领域对象和领域服务进行,其中涉及一个专门的术语——上下文

image71764511902478.png

需要注意的是 ,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应用,它有三个包和三个主要的类

asdiasdojasodjasiodjosa.png

我们可以使用下面的规则编写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层,并声明其约束关系。如果出现错误的依赖关系,测试就不会通过

官网使用了一张图来说明三层架构下的依赖关系,可以看到,这里只允许下层类被上层调用,以此来守护代码的架构。在编写本书时,官网的示例代码存在部分未更新的情况,如果按照官网的说明不能运行,可以参考本书提供的示例代码

image90000.png

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库来报告问题的,因此受制于该库的更新情况,存在一定程度上的滞后问题