不知道大家是否注意到,在 Spring Framework 7.x、Spring Boot 4.x 以及 Spring Cloud 2025.x 的版本发布说明中,都特别提到了对 @NonNull 注解或 JSpecify 的支持。这个注解并非新事物,为何又被重新提及?
今天,我们就来聊聊这背后的故事。这是一个关于 Java 生态如何终结 @NonNull 注解十年混乱,以及 JSR 305 如何“烂尾”,最终由大厂联盟推动 JSpecify 标准,并与 Spring 7 联手让这场“暗战”落幕的过程。
@NonNull:一个注解,多重选择
@NonNull 并非新注解,而是 Java 生态为解决空指针异常(NPE)问题而广泛使用的工具。许多 Java 开发者都遇到过这样的场景:当你想标记一个参数不允许为 null 时,IDE 的自动补全功能可能会列出多达 5 个不同的 @NonNull 供你选择:
javax.annotation.Nonnull
org.springframework.lang.NonNull
lombok.NonNull
androidx.annotation.NonNull
com.alibaba.nacos.shaded.javax.annotation.Nonnull
面对这一堆选项,难免让人困惑。更令人不解的是,当你使用 Nacos 自带的 @Nonnull 时,IDE 可能不会给出任何警告;但换成 Spring 的 @NonNull 后,黄色的警告提示立刻出现。难道 IDE 也会“区别对待”?
不妨通过下面的代码实验一下:
// 实验1:使用Spring的@NonNull
public void test(@org.springframework.lang.NonNull String a) {}
test(null); // IDEA警告:传递了null给非空参数
// 实验2:使用Nacos的@Nonnull
public void test(@com.alibaba.nacos.shaded.javax.annotation.Nonnull String a) {}
test(null); // IDEA安静如鸡,毫无反应
// 实验3:使用原生的javax.annotation.Nonnull
public void test(@javax.annotation.Nonnull String a) {}
test(null); // IDEA再次警告
为什么语义相同的注解,IDE 会有不同的反应?要解开这个谜团,需要从一个“休眠”了十多年的标准说起。
JSR 305:被遗忘的规范

图1:JCP官网上处于“Dormant(休眠)”状态的JSR 305页面
大家不妨检查一下自己的项目依赖里,是否有一个 jsr305.jar 文件。这个 JSR 305 规范,看起来并不像 Java 官方重点维护的“亲儿子”,反而像是被遗忘了。
当前 @NonNull 注解的混乱局面,其根源都指向一个编号:JSR-305(https://jcp.org/en/jsr/detail?id=305)。
什么是 JSR 305?
JSR 305(Java Specification Request 305)是2006年启动的一项Java规范提案,全称为“Annotations for Software Defect Detection”(软件缺陷检测注解)。其初衷是为 Java 引入一套标准的静态分析注解,以帮助 FindBugs、IDE 等工具检测潜在的代码缺陷,其核心就是定义 @Nonnull、@Nullable、@CheckForNull 等空指针相关注解。这些注解位于 javax.annotation 包下,由 jsr305.jar 提供。
JSR 305 的“休眠”之谜
然而,这个始于2006年的规范,在2025年的今天,其状态仍然显示为 “Dormant(休眠)”。这意味着:
- 无人维护:没有官方团队持续推动其演进。
- 设计缺陷:原始设计对泛型、数组、类型边界等复杂场景的支持不足。
- 生态分裂:各大厂商(Google、Spring、JetBrains、Alibaba等)被迫各自实现,导致互不兼容。
这种长期的“放养”状态,直接导致了 Spring、Nacos、Lombok 等框架不得不“重复造轮子”,各自定义 @NonNull 注解,但每个注解的“法律效力”(即工具链的支持程度)却各不相同。
JSpecify:社区驱动的救赎
既然官方的 JSR 305 迟迟无法“苏醒”,那么社区就自己创造一个新的标准。2019年,Google、JetBrains、Uber、Pivotal、Square 等巨头达成共识,联合发起了 JSpecify 项目。
JSpecify 是什么?
JSpecify 是一个全新的、独立于 JCP/JSR 流程的注解库项目,其使命非常明确:为 Java 提供一套精确、统一的空指针注解标准。
其官网定义简洁有力:
JSpecify provides precise, tool-friendly annotations for Java nullness analysis, enabling better static checking and Kotlin interoperability.
翻译过来即:JSpecify 为 Java 的空值分析提供了精确且对工具友好的注解,能够实现更好的静态检查并增强与 Kotlin 的互操作性。
它的核心优势包括:
- 彻底解决问题:支持对泛型参数、数组元素等进行精确的空指针注解。
// 可以精确到泛型参数
List<@NonNull String> nonNullElements;
// 数组元素的空性
String @NonNull [] nonNullArray;
- 规范明确:提供详细的语义规范,避免不同工具解释产生歧义。
- 强大的社区背书:由 Google、JetBrains、Microsoft、Spring 等顶级厂商联合维护。
- 零运行时依赖:纯注解库,不引入任何运行时开销。
- 完美的 Kotlin 互操作性:从根本上解决了 JSR305 注解在与 Kotlin 交互时的长期痛点。
里程碑时刻
2024年,JSpecify 发布了 1.0.0 正式版。这标志着 Java 空指针注解的混乱历史迎来了终结的曙光。
Spring 7 全面拥抱 JSpecify
Spring 官方在 Spring Framework 7.0 的发布说明中,用专门一个章节宣告了这一重要变革:
Spring nullness annotations with JSR 305 semantics are deprecated in favor of JSpecify annotations.
(带有 JSR 305 语义的 Spring 空值注解已被弃用,取而代之的是 JSpecify 注解。)
这仿佛是一份“独立宣言”,表明了 Spring 社区对陈旧标准的告别和对新标准的支持。
Spring 7 做了哪些改变?
- 全量迁移:Spring Framework 自身的代码库已将所有空指针注解迁移到了 JSpecify。
- 功能增强:支持更精确的注解,例如可以指定数组或可变参数中元素的空性。
- 提升 Kotlin 友好度:修复了因使用 JSR305 注解而在 Kotlin 中导致的空安全(nullability)不一致问题。
- 平滑过渡:原有的
spring-core 模块中的 @NonNull、@Nullable 注解已被标记为 @Deprecated,但会保留以保证向后兼容性。
Spring 官方给出的迁移理由非常充分:
- JSR305 处于休眠状态,缺乏维护。
- JSpecify 提供了更精确、更规范的语义定义。
- 能获得更好的工具链支持(如 IntelliJ IDEA、ErrorProne、NullAway)。
- 彻底解决了与 Kotlin 的互操作问题。
使用与迁移示例
在 Spring 6.x 或更早版本中,你可能会这样写:
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
public class UserService {
public User getUser(@NonNull String id) {
return userRepository.findById(id);
}
}
而在 Spring 7.x 中,推荐的做法是使用 JSpecify 的注解:
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
public class UserService {
public User getUser(@NonNull String id) {
return userRepository.findById(id);
}
}
可以看到,主要变化是导入的包名从 org.springframework.lang 变为了 org.jspecify.annotations。迁移后,IDE 和各类静态分析工具对代码的检查将更加精确和一致。
IDEA“区别对待”的真相
现在,我们可以完整解释文章开头那个实验现象背后的原因了。关键在于不同注解的实现方式:
// Nacos的@Nonnull实现(示例)
@Retention(RetentionPolicy.CLASS)
public @interface Nonnull {} // 注解本身没有携带任何元数据信息
// JSR305的@Nonnull实现(示例)
@Retention(RetentionPolicy.RUNTIME)
public @interface Nonnull {} // 同样“光秃秃”
// Spring的@NonNull实现(关键)
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@org.springframework.lang.NonNull // Spring自己的注解
@javax.annotation.Nonnull // 关键:元注解,声明自己符合JSR305语义
public @interface NonNull {
}
核心原因如下:
- IDE 的硬编码识别:IntelliJ IDEA 等工具早期内置了对
javax.annotation.Nonnull 这个特定注解的硬编码支持。
- Spring 的兼容策略:为了让自己的注解被广泛识别,Spring 的
@NonNull 添加了 @javax.annotation.Nonnull 作为元注解(meta-annotation)。这相当于告诉工具:“我的语义和 JSR305 的一样”。
- Nacos 注解的“孤立”:Nacos shaded 包中的
@Nonnull 没有添加任何表明其语义的元信息,因此 IDE 无法识别它应该触发空指针检查。
这种“区别对待”恰恰暴露了 JSR305 生态的最大问题:缺乏在工具层面的统一、可扩展的识别标准。
总结:从混乱走向统一
纵观整个时间线:
- 2006年:JSR 305 启动。
- 2012年:JSR 305 进入休眠状态,生态开始分裂。
- 2019年:JSpecify 项目启动,由行业巨头联合推动。
- 2024年:JSpecify 1.0.0 正式版发布。
- 2025年:Spring Framework 7.0 正式采用 JSpecify,宣告 JSR 305 时代的终结。
这表面上是技术标准的自然演进,更深层是社区协作力量的体现。当官方的 JCP 流程无法推动一个休眠的标准时,领先的企业和开发者选择绕开官僚流程,用代码和协作创造了一个更优的解决方案。
对 Java 开发者的意义:
- 更少的困惑:未来只需认准
org.jspecify.annotations 这一个包。
- 更强的静态检查:工具链能更精准地发现潜在的空指针安全问题。
- 更好的互操作性:为 Java 与 Kotlin 的混合编程扫清了一大障碍。
- 更统一的标准:由主要厂商共同维护,前景更加稳定可靠。
org.jspecify.annotations 这个包名,将成为未来 Java 空指针安全领域的代名词。对于开发者而言,了解 Spring 框架的这次重要变更,有助于我们更好地构建健壮的应用程序,并跟上生态发展的步伐。想了解更多最新的技术动态和深度解析,欢迎访问云栈社区。
参考资料
- Spring Framework 7.0 Release Notes:
https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-7.0-Release-Notes
- JSpecify 官方用户指南:
https://jspecify.dev/docs/user-guide/
- JSR 305 规范页面:
https://jcp.org/en/jsr/detail?id=305