❓ 问题:为什么 NPE 是“幽灵 bug”?
// 传统写法:返回值可空?没人知道!
public User findUser(String id) {
return userRepository.findById(id); // 可能返回 null
}
// 调用处:
String name = findUser("123").getName(); // 💥 运行时 NPE!
🔍 根源:Java 类型系统无法区分 User 和 User?(可空用户)。
✅ 解决方案:JSpecify + @NullMarked
Spring Boot 4 全面采用 JSpecify——新一代空安全注解标准,核心思想:
默认所有类型非空,只标注例外(
@Nullable)
🧱 三步开启空安全
第 1 步:设置包默认非空
在 src/main/java/com/example/demo 下创建 package-info.java:
@NullMarked // 👈 整个包默认:所有类型非空!
package com.example.demo;
import org.jspecify.annotations.NullMarked;
⚠️ 注意:每个包需单独声明(不继承子包)。
第 2 步:标注“真的可能为空”的地方
@Service
public class UserService {
// 明确标注:可能返回 null
@Nullable
public User findUserById(Long id) {
return userRepo.findById(id).orElse(null);
}
// 参数可能为空:标注 @Nullable
public void logAction(@Nullable String action) {
if (action != null) {
System.out.println("Action: " + action);
}
}
}
✅ 语义清晰:没标 @Nullable 的,就是“一定不为 null”。
第 3 步:安全使用(编译器帮你检查!)
@RestController
public class UserController {
@GetMapping("/user/{id}")
public String getUserName(@PathVariable String id) {
User user = userService.findUserById(Long.valueOf(id)); // ← 返回 @Nullable
// ❌ 旧写法:直接调用 → 编译报错!
// return user.getName(); // 💥 Error: dereferencing @Nullable expression
// ✅ 正确写法:必须判空
if (user != null) {
return user.getName();
}
return "用户不存在";
}
}
🔧 配合 NullAway 插件 → 上述错误在 编译阶段就拦截,不等到上线!
📦 集合中的空元素?也能标注!
// 列表本身非空,但元素可能为空
public List<@Nullable String> getComments() {
return List.of("好评!", null, "会回购");
}
// 使用时必须处理 null 元素
public long getValidCommentCount(List<@Nullable String> comments) {
return comments.stream()
.filter(Objects::nonNull) // ← 显式过滤
.count();
}
🆚 @Nullable vs Optional?怎么选?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 返回值 | Optional<User>(新 API)@Nullable User(老 API 迁移) |
Optional 更函数式;@Nullable 无对象开销 |
| 方法参数 | @Nullable String name |
Optional<String> 做参数很别扭 |
| 高性能路径 | @Nullable |
零运行时成本 |
✅ 最佳实践:
// 新接口用 Optional
public Optional<User> findUserOpt(Long id) {
return userRepo.findById(id);
}
// 老接口加 @Nullable(兼容 + 安全)
@Nullable
public User findUser(Long id) {
return userRepo.findById(id).orElse(null);
}
🛠️ 附加:快速集成 NullAway(Maven)
在 pom.xml 中加入(Spring Boot 3.2+ 兼容):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>0.12.7</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
✅ 加上之后:
user.getName()若user可能为 null → Maven 构建直接失败!
✅ 总结:三句话记住核心
@NullMarked→ 本包默认:所有类型非空@Nullable→ 显式标注:只有这里可能为空- NullAway → 编译期守门员:不让 NPE 溜进生产环境
🎯 效果:空指针异常从 “线上事故” 变成 “本地编译错误” —— 修复成本下降 100 倍!