1 前言

从Spring 5.0.0.RELEASE版本开始,如果细心查看源码,会发现源码中许多位置( Field、方法返回值、方法形参 )都可能标注了 @org.springframework.lang.Nullable 注解,包下面也存在相应的 page-info 文件。这是Spring框架引入的Null Safety特性。

许多语言都存在臭名昭著的null问题,java也不例外,而且java编译器不会对潜在的null调用发出警告。虽然一个团队可以强调自己的编码规范,并进行一定程度上的人工评审(如组内互相review),但这始终是将决定权交给了每个程序员,在实践上无法形成有效规范的流程,在CI/CD中显得乏力。

由此涌现出一批静态代码分析工具,分析java源码中潜在的null隐患以及其他缺陷,甚至包括代码风格的检查。Null Safety最早就是由静态代码分析工具提供的注解集,让开发者标识需要进行null隐患扫描的位置 ( Field、方法返回值、方法形参等 ),以便静态分析工具能快速识别和定位null缺陷。

此外Null Safety也是给开发人员自己来看的,例如其他人开发的api,如何快速正确的使用而不用担心导致空指针。原来的办法可能是看注释说明或者把api实现逻辑看一遍,无论哪一种都很麻烦,而形参上的一个 @Nullable 注解即可说明。

2 Null Safety

FindBugs和JetBrains均提供了自己的Null Safety注解集,同时在JSR官网中也查到了305规范提案,但目前仍处于搁置暂停状态。另外再查看了Spring、Guava中的Null Safety,可以发现FindBugs定义的Null Safety注解集可以说是事实标准了。因此现在说JSR305一般指的是 com.google.code.findbugs:jsr305

2.1 JSR305

即FindBugs提供的注解集 com.google.code.findbugs:jsr305。较为常用的注解包括以下这些:

@TypeQualifier

/**
 * 该限定符应用于注解,以表示该注解应被视为类型限定符。
 */
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeQualifier {

    /**
     * 描述可以应用限定符的值的种类。如果提供了数字类(例如Number.class或Integer.class),
     * 则注解也可以应用于相应的原始数字类型。
     */
    Class<?> applicableTo() default Object.class;

}

@TypeQualifierDefault

/**
 * 该限定符应用于注释,以表示该注释定义了默认类型限定符,该默认类型限定符在其所应用的元素范围内可见。
 */
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeQualifierDefault {
    ElementType[] value() default {};
}

@Nonnull

/**
 * 带注释的元素不能为null。
 * 构造完成后,带注释的字段不得为空。
 * 当将此注释应用于方法时,它将应用于方法的返回值。
 */
@Documented
@TypeQualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Nonnull {
    When when() default When.ALWAYS;

    class Checker implements TypeQualifierValidator<Nonnull> {

        public When forConstantValue(Nonnull qualifierArgument, Object value) {
            if (value == null)
                return When.NEVER;
            return When.ALWAYS;
        }
    }
}

@Nullable

/**
 * 在某些情况下,带注释的元素可以为null。
 * 通常,这意味着开发人员将必须阅读文档以确定何时可以接受空值,以及是否有必要检查空值。
 *
 * 该注释主要用于覆盖Nonnull注释。静态分析工具通常应将带注释的项目视为没有注释,除非将它们配置为
 * 最大程度地减少假阴性。使用CheckForNull表示应始终检查元素值是否为空值。
 *
 * 当将此注释应用于方法时,它将应用于方法的返回值。
 */
@Documented
@TypeQualifierNickname
@Nonnull(when = When.UNKNOWN)
@Retention(RetentionPolicy.RUNTIME)
public @interface Nullable {}

2.2 Spring中的Null Safety

Spring framework从5.0.0.RELEASE版本开始,也提供了自己的Null Safety注解,该注解是JSR305注解的扩展。主要有 @NonNull@Nullable@NonNullApi@NonNullFields

其中 @NonNull@Nullable 专门用于方法返回值、方法形参、Field字段等位置。

@NonNullApi@NonNullFields 注解专门用于 page-info 文件,标识默认情况下该包中类的所有方法返回值、方法形参、Field字段位置均要求非空。如果该包下有某些方法返回值、方法形参、Field字段位置需求可为空,就单独使用 @Nullable 注解标识 ( @Nonnull 注解基本不用 )。

page-info 文件中的使用示例如下:

@NonNullApi
@NonNullFields
package com.xx.xx;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

IDE以及Sonar等开源工具均能识别这些注解,并在静态扫描中检查是否存在null隐患。

@NonNull

/**
 * 一个通用的Spring注释,用于声明被注释的元素不能为null。
 *
 * 在支持JSR-305的通用工具上利用JSR-305元注释来指示Java中的可空性,
 * 并且Kotlin使用这些工具来推断Spring API的可空性。
 *
 * 应在参数,返回值和字段级别使用。方法覆盖应该重复父@NonNull注释,除非它们的行为不同。
 *
 * 使用@NonNullApi(作用域=参数+返回值)和/或{@code @NonNullFields}(作用域=字段)
 * 将默认行为设置为不可为空,以避免用注释整个代码库@NonNull。
 */
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull   // 引用了JSR305注解
@TypeQualifierNickname  // 引用了JSR305注解
public @interface NonNull {}

@NonNullApi

/**
 * 一个通用的Spring注解,用于声明对于给定的包,参数和返回值默认被视为不可为空。
 *
 * 在支持JSR-305的通用工具上利用JSR-305元注释来指示Java中的可空性,
 * 并且Kotlin使用这些工具来推断Spring API的可空性。
 *
 * 应该在包级别与参数和返回值级别的Nullable注解一起使用。
 */
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull  // 引用了JSR305注解
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})  // 引用了JSR305注解
public @interface NonNullApi {}

@NonNullFields

/**
 * 一个通用的Spring注释,用于声明对于给定的包,默认情况下字段将被视为不可为空。
 *
 * 在支持JSR-305的通用工具上利用JSR-305元注释来指示Java中的可空性,
 * 并且Kotlin使用这些工具来推断Spring API的可空性。
 */
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull  // 引用了JSr305注解
@TypeQualifierDefault(ElementType.FIELD)  // 引用了JSR305注解
public @interface NonNullFields {}

@Nullable

/**
 * 一个通用的Spring注释,用于声明被注释的元素在某些情况下可以为null。
 *
 * 在支持JSR-305的通用工具上利用JSR-305元注释来指示Java中的可空性,
 * 并且Kotlin使用这些工具来推断Spring API的可空性。
 *
 * 应在参数,返回值和字段级别使用。 重写方法应重复父@Nullable批注,除非它们的行为不同。
 *
 * 可以与@NonNullApi或@NonNullFields结合使用,以将默认的不可为空的语义覆盖为可为空。
 */
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull(when = When.MAYBE)  // 引用了JSR305注解
@TypeQualifierNickname  // 引用了JSR305注解
public @interface Nullable {}

2.3 Guava中的Null Safety

以Guava 20.0版本为例,它没有自定义Null Safety注解,而是直接使用了JSR305注解集。如Guava IO包下的 page-info 文件为例:

@CheckReturnValue  // 表明包中类的方法返回值默认情况下不能为空
@ParametersAreNonnullByDefault  // 表明包中类的方法形参默认情况下不能为空
package com.google.common.io;

import javax.annotation.CheckReturnValue;
import javax.annotation.ParametersAreNonnullByDefault;

3 静态扫描分析工具与规则插件

静态扫描工具的作用是将源码或者编译后的代码转为抽象语法树 ( AST ),以供配置的规则插件进行静态代码分析,最终以不同方式呈现出扫描结果 ( 例如语法高亮、输出缺陷信息文档 )。Eclipse、IDEA等IDE都内置的静态扫描分析工具,而开源的分析工具有PMD、Sonar等。

规则插件是特定于静态扫描工具的,规则约束上相同,但插件实现上不同。规则本身可能是针对代码缺陷,也可能针对代码风格,甚至两者兼具。例如阿里Java规约插件,有PMD、Eclipse、IDEA版本,Github上也有网友提供的Sonar版本实现。

本地开发时,我们更喜欢直接使用基于IDE进行静态扫描分析 ( 或者在IDE高亮提示时进行处理 ),但是在CI/CD中,需要单独部署一套静态扫描分析工具,以便记录当前静态代码扫描情况。

3.1 阿里Java规则插件

该插件主要和阿里Java规约配合使用,属于代码风格层面的规则插件,用于识别不符合阿里Java规约描述的代码风格,例如魔法值。

至于说为何这款插件是代码风格层面的规则插件,因为它不兼容JSR305的注解集,而且在本地试验时,对于一些潜在的null情况扫描不出来。

public class Demo {

    public static void main(String[] args) {
        String something = Demo.getSomething(); // IDEA会给出语法高亮,但阿里插件扫描结果没有说明
    }

    // @javax.annotation.Nonnull // 加上该注解时,阿里插件扫描结果也没有说明
    public static String getSomething() {
        return null;
    }

}

3.2 FindBugs插件

不知道该称呼为扫描工具还是规则插件,没有细看基于IDEA的FindBugs插件的内部结构,但我觉得既然是IDEA插件,应该会复用IDEA自身的代码扫描功能,仅做上层规则处理更精巧省事。

FindBugs插件用于处理代码缺陷层面的问题,用于配合 com.google.code.findbugs:jsr305 ( 名称上就一样 ),主要用于处理null问题。不过这款IDEA上的FindBugs插件不支持基于jsr305注解的扩展,例如Spring的Null Safety注解就无法生效。以下是使用 FindBugs-IDEA 1.0.1 以及 com.google.code.findbugs:jsr305:3.0.2 测试的情况说明:

1、什么注解也不加,测试对null值的扫描情况 ( 可以正常扫描,并且提示的错误位置是正确的 ):

java项目中的静态化解析 java代码静态分析_java项目中的静态化解析

2、在调用方法的形参上添加 @javax.annotation.Nullable 注解,测试对null值的扫描情况 ( 可以正常扫描,并且提示的错误位置是正确的,因为形参标注了Nullable,因此对null值的处理应该由 doSomeThing() 方法负责 – PS: findbugs也支持JetBrains的同类型注解,并且有效 ):

java项目中的静态化解析 java代码静态分析_java项目中的静态化解析_02

3、在调用方法的形参上添加 @org.springframework.lang.Nullable 注解,测试对null值的扫描情况 ( 扫描提示的错误位置不正确,应该和上面基于 @javax.annotation.Nullable 注解的扫描情况一致才对。这意味着如果我们使用自定义的null safety注解,使用这个扫描时无法定位应该被修改地方 ):

java项目中的静态化解析 java代码静态分析_java项目中的静态化解析_03

3.3 SonarLint插件

不知道该称呼为扫描工具还是规则插件,没有细看基于IDEA的SonarLint插件的内部结构,但我觉得既然是IDEA插件,应该会复用IDEA自身的代码扫描功能,仅做上层规则处理更精巧省事。

SonarLint插件可以处理代码缺陷和代码风格问题,而且不存在FindBugs插件那样不兼容的问题,在本地开发中是最合适的。

3.4 静态扫描分析工具

CI/CD中这个应该是必备的,开源中比较热门的是Sonar、PMD等,重点是要根据自己的需求选择合适的代码缺陷、代码风格处理规则

下面介绍基于IDEA命令行的静态代码扫描分析,实际中不知道用的多不多,但可以实现,能作为一个备选项或参考。

3.4.1 基于IDEA命令行的静态代码扫描分析

IDEA自带的代码扫描提示功能是非常强大的,语法高亮等功能非常完善,因此设想是否能将这些高亮提示等输出为文件,能单独查看,从而变相实现一个CI/CD所需的静态代码扫描记录功能。正好IDEA官网上有关于命令行的处理方式,支持Windows、MacOs、Linux,需要顾虑的可能就是版权以及对IDEA的依赖程度过高。

按照官网的指示,启动命令中指定检查所需的配置信息,即可得到一些xml文件,每个文件名都指示了代码存在的问题。不过目前也仅提供这些信息,缺少对代码缺陷、代码风格的等级分类和统计、存储等功能,需要自己开发实现。

java项目中的静态化解析 java代码静态分析_java项目中的静态化解析_04

java项目中的静态化解析 java代码静态分析_java_05