Spring Boot4正式篇:第三篇 空安全应用

88 阅读5分钟
日期更新说明
2025年12月3日初版完成
2025年12月6日案例编写

前言

如果编程语言最容易出现的错误,那么 NPE(NullPointerException)绝对算一个了;让我们时光追溯到1965年,霍尔在设计ALGOL W语言时首次引入空引用(null),初衷是简化编译器实现,空引用性就成为一种零成本的抽象,用于表达值的潜在缺失,并且与现有 API 向后兼容;但在他在2009年提出是一个十亿美元的错误’”(Billion-Dollar Mistake)。rust甚至放弃了 null 的 设计,对于 javaer从来没有放弃过努力。

由来

规避 NPE 最好的方式就是在编译阶段明确,“空注解”应运而生加上静态扫描的工具,基本可以解决。但在此之前,各种“可空性注解”群魔乱舞

注解来源示例状态
JSR-305@Nonnull, @Nullable废案(JSR 305 没有最终版)
JetBrains@NotNull, @NullableIntelliJ 私有实现
Checker Framework@NonNull, @Nullable学术/工业项目使用
Android Support / AndroidX@NonNull, @NullableAndroid 生态使用
Lombok@NonNullLombok 专用
Eclipse JDT@NonNull, @NullableEclipse 专用

大概在 2019–2022 期间,Google 主导并联合:

  • JetBrains(IntelliJ)
  • Checker Framework 团队(学术界)
  • Kotlin 核心成员(提供 null-safety 经验)
  • ErrorProne / NullAway 团队
    → NullAway 基于 ErrorProne,是 Google 内部广泛使用

Java 的官方 Nullability 标准(事实上的标准);为什么说实施标准呢?第一次形成了跨工具的一致性(所谓的“生态”)

  • ErrorProne / NullAway
  • IntelliJ
  • Eclipse
  • Checker Framework
  • AndroidX

都可以用同一个注解解释 nullability。

时间事件
2019Google 与学界开始制定初版规范
2021首批 draft 发布
2022NullAway / Checker Framework 开始部分支持
2023JSpecify 1.0 beta 版本发布
2024JSpecify 1.0.0 正式版发布,进入生产可用阶段
2024–2025Spring Boot 4 等现代框架开始推荐 JSpecify

本质

JSpecify 为 Java 生态提供一个跨工具统一的 Nullability 类型语义标准,使静态分析器能够在编译期进行一致的 null 审查,并为未来 Java 官方的 null-safety 语言特性铺路。

JSpecify 的设计哲学是 “默认非空(Null-Marked),只标注例外,而不是标注正常情况。 ” ;这个独到的设计哲学符合我们的编程方式,仔细想想,空值不是常态才是。JSpecify 非常克制,只包含 4 个核心注解

注解作用属于哪一类
@Nullable标注某个类型可以接受 null成员注解
@NonNull标注某个类型不允许为 null成员注解
@NullMarked将整个包或类默认为 NonNull(默认非空)默认注解
@NullUnmarked将子包/类重新设回“无默认规则”默认注解

使用

SpringBoot4 JSpecify 官方使用案例

JacksonJsonParser 为例:

JacksonJsonParser提前剧透下一篇更新关于 Jackson3的内容哈。

案例程序

github.com/will-we/blo…

注意切换到jspecify分支

其实开发过程中 IDE 工具基本可以提示你的错误;注意注释部分内容:

到这你以为结束了,国内其他博客可能会,而我这不会。

编译构建

mvn -DskipTests=true compile

关键部分内容:

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <excludes>
          <exclude>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
          </exclude>
        </excludes>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.14.1</version>
      <configuration>
        <release>17</release>
        <encoding>UTF-8</encoding>
        <fork>true</fork>
        <compilerArgs>
          <arg>-XDcompilePolicy=simple</arg>
          <arg>--should-stop=ifError=FLOW</arg>
          <arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked -XepOpt:NullAway:JSpecifyMode=true</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
          <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
          <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
          <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
        </compilerArgs>
        <annotationProcessorPaths>
          <path>
            <groupId>com.google.errorprone</groupId>
            <artifactId>error_prone_core</artifactId>
            <version>2.42.0</version>
          </path>
          <path>
            <groupId>com.uber.nullaway</groupId>
            <artifactId>nullaway</artifactId>
            <version>0.12.12</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>
  • Error Prone(Google 的 Java 静态分析器)最核心、最重要的模块。如果你要启用 ErrorProne 或 NullAway,它几乎一定会出现在你的依赖里
  • NullAway = Java 的“快速、实用、接近 Kotlin 级别”的空指针静态检查器
    • 几乎零误报
    • 超快(对大型项目几乎无编译损耗)
    • 与 JSpecify 注解兼容
    • Google 和 Uber 大规模生产实践

超低的误报率:

工具特点误报率
Checker Framework非常强但非常重❗高
FindBugs / SpotBugs老工具,误报多❗高
IntelliJ Nullability轻量,分析不完全
NullAway平衡度极好,生产环境友好最低

缺点:

需要 JDK22+;另外对于使用 lombok不太友好。

注意 NullAway JSpecify 模式需要较新的 javac 版本,因此我们建议如果可以的话使用 Java 25,否则大多数 JDK 21.0.8+发行版(除 Oracle JDK 外)应该支持 -XDaddTypeAnnotationsToSymbol=true 标志,这将允许 NullAway 按预期工作。未来可能会提供这个标志的 Java 17 后端。如果像 Spring 一样需要保留 Java 17 基线,你可以使用 Java 25 工具链,并像jspecify-nullaway-demo.中所示那样配置 Maven 或 Gradle 构建的 javac 选项 --release 17

总结

当 Spring Boot 4 发布并在您的应用程序中使用时,特别是如果您在应用级别也启用了这些空值检查,那么在生产环境中 NullPointerException 的风险将显著降低甚至消除,因为只有来自第三方库的类型才可能导致这种情况。通过明确指定可能发生空引用的位置、处理这些代码路径并引入相关的自动检查,我们将“价值十亿美元的错误”转化为零成本的抽象,允许表达值的潜在缺失,从而显著提高 Spring 应用程序的安全性。

补充

截至发文(2025年12月6日)spring官方完成如下内容:

以下项目现在提供 null-safe API:

  • Spring Boot 4.0
  • Spring Framework 7.0
  • Spring Data 4.0
  • Spring Security 7.0
  • Spring Batch 6.0
  • Spring Kafka 4.0
  • Spring Integration 7.0
  • Spring GraphQL 2.0
  • Spring Web Services 5.0
  • Spring AMQP 4.0
  • Spring Shell 4.0
  • Spring Kafka 4.0
  • Spring Plugin 4.0
  • Spring HATEOAS 3.0
  • Spring Modulith 2.0
  • Spring Vault 4.0
  • Spring Cloud Commons 5.0
  • Spring Cloud Gateway 5.0
  • Micrometer 1.16
  • Micrometer Tracing 1.6
  • Context Propagation 1.2
  • Reactor 2025.0
    有些 Spring 项目尚未提供空安全 API,但计划在不久的将来提供:
  • Spring AI (planned in 2.0)
    Spring AI(计划在 2.0 版本)
  • Spring Session
  • Spring LDAP
  • Spring gRPC (tentatively planned in 1.0)
    Spring gRPC (计划在 1.0 版本中推出)
  • The rest of Spring Cloud (tentatively planned in 2026.0)
    Spring Cloud 的其余部分 (计划在 2026.0 版本中推出)

扩展阅读: