NullPointerExceptions (通常缩写为“NPE”)对于每个 Java 程序员来说都是一场噩梦。

我们可以在互联网上找到大量解释如何编写空安全代码的文章。空安全确保我们在代码中添加了适当的检查,以保证对象引用不能为空,或者毕竟在对象为空时采取可能的安全措施。

由于 NullPointerException 是一个运行时异常,因此在代码编译过程中很难找出这种情况。 Java 的类型系统没有办法快速消除危险的空对象引用。

幸运的是,Spring 框架提供了一些注释来解决这个问题。在本文中,我们将学习如何使用这些注释通过 Spring Boot 编写空安全代码。

示例代码

本文附有 GitHub 上的工作代码示例

Spring 中的空安全注解

在 Spring 核心包 org.springframework.lang 下,有 4 个这样的注解:

  • @NonNull, @NonNull
  • @NonNullFields, @NonNullFields
  • @Nullable,
  • @NonNullApi.

Eclipse 和 IntelliJ IDEA 等流行的 IDE 可以理解这些注释。它们可以在编译期间警告开发人员潜在的问题。

我们将在本教程中使用 IntelliJ IDEA。让我们通过一些代码示例来了解更多信息。

要创建基础项目,我们可以使用 Spring Initializr。 Spring Boot 启动器就是我们所需要的,不需要添加任何额外的依赖项。

IDE 配置

请注意,并非所有开发工具都可以显示这些编译警告。如果您没有看到相关警告,请检查 IDE 中的编译器设置。

IntelliJ

对于 IntelliJ,我们可以在“Build, Execution, Deployment -> Compiler”下激活注释检查:

IntelliJ compiler config

Eclipse

对于 Eclipse,我们可以在“Java -> Compiler -> Errors/Warnings”下找到设置:

Eclipse compiler config

示例代码

让我们使用一个简单的 Employee 类来理解注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package io.reflectoring.nullsafety;

// imports

class Employee {
  String id;
  String name;
  LocalDate joiningDate;
  String pastEmployment;

  // standard constructor, getters, setters
}

@NonNull

大多数情况下, id 字段(在 Employee 类中)将是一个不可为 null 的值。因此,为了避免任何潜在的 NullPointerException 我们可以将此字段标记为 @NonNull

1
2
3
4
5
6
7
class Employee {

  @NonNull
  String id;

  //...
}

现在,如果我们不小心尝试在代码中的任何位置将 id 的值设置为 null,IDE 将显示编译警告:

IDE warning for NonNull

@NonNull 注释可以在方法、参数或字段级别使用。**

此时,您可能会想“如果一个类有多个非空字段怎么办?”。如果我们必须在每一个之前添加 @NonNull 注释,是不是太罗嗦了?

我们可以通过使用 @NonNullFields 注释来解决这个问题。

以下是 @NonNull 的快速摘要:

带注释的元素影响
field当字段为空时显示警告
parameter当参数为空时显示警告
method当方法返回 null 时显示警告
package不适用

@NonNullFields

让我们创建一个 package-info.java 文件以在包级别应用非空字段检查。该文件将包含带有 @NonNullFields 注释的根包名称:

1
2
3
4
@NonNullFields
package io.reflectoring.nullsafety;

import org.springframework.lang.NonNullFields;

现在,我们不再需要使用 @NonNull 注释来注释字段。因为默认情况下,该包中类的所有字段现在都被视为非空。而且,我们仍然会看到与以前相同的警告:

IDE warning for NonNullFields

这里要注意的另一点是,如果有任何未初始化的字段,那么我们将看到初始化这些字段的警告:

IDE warning for NonNull

以下是 @NonNullFields 的快速摘要:

带注释的元素影响
field不适用
parameter不适用
method不适用
package如果所应用的包的任何字段为空,则显示警告

@NonNullApi

到目前为止,您可能已经发现了另一个要求,即对方法参数或返回值进行类似的检查。 @NonNullApi 将会来拯救我们。

@NonNullFields 类似,我们可以使用 package-info.java 文件并为目标包添加 @NonNullApi 注释:

1
2
3
4
@NonNullApi
package io.reflectoring.nullsafety;

import org.springframework.lang.NonNullApi;

现在,如果我们编写方法返回 null 的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package io.reflectoring.nullsafety;

// imports

class Employee {

  String getPastEmployment() {
    return null;
  }

  //...
}

我们可以看到 IDE 现在警告我们有关不可为 null 的返回值:

IDE warning for NonNullApi

以下是 @NonNullApi 的快速摘要:

带注释的元素影响
field不适用
parameter不适用
method不适用
package如果所应用的包的任何参数或返回值为空,则显示警告

@Nullable

但这里有一个问题。在某些情况下,特定字段可能为空(无论我们多么想避免它)。

例如, pastEmployment 字段在 Employee 类中可以为空(对于以前没有工作过的人)。但根据我们的安全检查,IDE 认为不可能。

我们可以使用字段上的 @Nullable 注释来表达我们的意图。这将告诉 IDE 该字段在某些情况下可以为空,因此无需触发警报。正如 JavaDoc 所建议的:

可以与 @NonNullApi@NonNullFields 结合使用,将默认的不可为空语义覆盖为可为空。

NonNull 类似, Nullable 注释可以应用于方法、参数或字段级别。

我们现在可以将 pastEmployment 字段标记为可为空:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.reflectoring.nullsafety;

// imports

class Employee {

  @Nullable
  String pastEmployment;

  @Nullable String getPastEmployment() {
    return pastEmployment;
  }

  //...
}

以下是 @Nullable 的快速摘要:

带注释的元素影响
field表示该字段可以为空
parameter表示参数可以为空
method表示该方法可以返回 null
package不适用

自动构建检查

到目前为止,我们正在讨论现代 IDE 如何使编写空安全代码变得更容易。然而,如果我们想在构建管道中进行一些自动代码检查,这在某种程度上也是可行的。

SpotBugs(著名但已废弃的 FindBugs 项目的转世)提供了一个 Maven/Gradle 插件,可以检测由于可空性而导致的代码异味。让我们看看如何使用它。

对于 Maven 项目,我们需要更新 pom.xml 以添加 SpotBugs Maven 插件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<plugin>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs-maven-plugin</artifactId>
  <version>4.5.2.0</version>
  <dependencies>
    <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
    <dependency>
      <groupId>com.github.spotbugs</groupId>
      <artifactId>spotbugs</artifactId>
      <version>4.5.3</version>
    </dependency>
  </dependencies>
</plugin>

构建项目后,我们可以使用该插件的以下目标:

  • spotbugs 目标分析目标项目。

  • check 目标运行 spotbugs 目标,如果发现任何错误,则使构建失败。

如果您使用 Gradle 而不是 Maven,则可以在 build.gradle 文件中配置 SpotBugs Gradle 插件:

1
2
3
4
5
6
7
dependencies {
  spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.11.0'
}

spotbugs {
  toolVersion = '4.5.3'
}

项目更新后,我们可以使用 gradle check 命令运行检查。

SpotBugs 提供了一些规则,通过在 Maven 构建期间处理 @NonNull 注释来标记潜在问题。您可以查看错误描述的详细列表

例如,如果任何用 @NonNull 注释的方法意外返回 null,则 SpotBugs 检查将失败,并显示类似以下内容的错误:

1
[ERROR] High: io.reflectoring.nullsafety.Employee.getJoiningDate() may return null, but is declared @Nonnull [io.reflectoring.nullsafety.Employee] At Employee.java:[line 36] NP_NONNULL_RETURN_VIOLATION

结论

这些注解对于 Java 程序员来说确实是一个福音,可以减少运行时出现 NullPointerException 的可能性。但是请记住,这并不能保证完全的空安全。

Kotlin 使用这些注释来推断 Spring API 的可为空性。

我希望您现在已经准备好在 Spring Boot 中编写 null 安全代码!

原文链接:https://reflectoring.io/spring-boot-null-safety-annotations/