Kotlin 2.1 + Spring Boot 4:JSpecify 带来的严格空安全检查

1. 现象描述

随着 Spring Boot 4Kotlin 2.1 的升级,在我代码中原本正常的泛型封装代码突然编译报错了。

先看一下具体场景,很多时候大家都会对 RestTemplateRestClient 进行一下封装,设置一些通用的请求参数,如鉴权。

1
2
3
4
5
6
7
8
9
10
// 这是一个简单的RestTemplate封装
fun <T> request(
url: String,
method: HttpMethod,
entity: HttpEntity<*>?,
responseType: ParameterizedTypeReference<T>,
vararg uriVariables: Any
): ResponseEntity<T> {
return restTemplate.exchange(url, method, entity, responseType, *uriVariables)
}
1
2
3
4
5
6
// 这是一个简单的RestClient封装
private fun <T> request(
responseType: ParameterizedTypeReference<T>
): T? {
return restClient.get().uri("...").retrieve().body(responseType)
}

但在升级 Spring Boot 4 和 Kotlin 2.1 之后,编译时发现出现了以下的错误。

1
2
Type argument is not within its bounds: must be subtype of 'Any'

而且错误的位置竟然是在参数上

1
2
3
4
5
private fun <T> request(
responseType: ParameterizedTypeReference<T> // <==这里报错
): T? {
return restClient.get().uri("...").retrieve().body(responseType)
}

这是怎么回事呢?

2. 核心原因

这次报错是由两个底层重大变更共同导致的:

  1. Spring Boot 4 引入 JSpecify 规范
  2. Kotlin 2.1 把 JSpecify 的 null 检查改为严格

1. Spring Boot 4 引入 JSpecify 规范

首先什么是 JSpecify ?

曾经在 Java 里使用标注类型安全的 API,比如 @NonNull@Nullable,会发现有无数的包都提供了这两个注解,至少有 javax(jakarta)的、 Lombok 的、JetBrains 的。

alt text

但不管用哪个,总的来说它只是在 IDE 里(可能)会给你做出一点提示,但在编译器不会有效果。

既然 Java 本身一直都没有原生提供这个 Null 检查的能力,几家大厂联合起来就推出了一个 JSpecify 规范,以后这种 @NonNull@Nullable 都用 JSpecify 的就没错了。

Spring Boot 4 基于的 Spring Framework 7 就全面引入了 JSpecify 的注解,并且在 package-info.java 中添加了 @NullMarked 注解,表示这个包下的所有类和接口都是严格 null 安全的。

所以,ParameterizedTypeReference<T> 的泛型参数 T 现在是非空类型的。

注:如果要定义一个可空的泛型,现在需要这么做 (Java)

1
public class NumberList<E extends @Nullable Number> implements List<E> {...}

B. Kotlin 2.1 把 JSpecify 的 null 检查改为严格

Kotlin 语言是由 JetBrains 开发的,而 JSpecify 其中重要的一员也是 JetBrains,那么它肯定要让 Kotlin 也能从 JSpecify 中收益。

在 Kotlin 2.1 之前,Kotlin 编译器对 Java 库中的 JSpecify 注解(如 @NonNull, @Nullable)通常只报 Warning
但从 Kotlin 2.1 开始,为了提升类型安全性,编译器默认将这些不匹配提升为 Error

C. Kotlin 泛型的默认行为

还有容易忽略的一点:

  • 在 Kotlin 中,<T> 实际上等同于 <T : Any?> ,它是可空的。
  • 而一个 @NullMarked 的 Java 泛型,<T> 等于 <T : Any>,即非空的。

3. 解决方案

添加显式的类型声明即可。需要明确告诉编译器,这个泛型 T 是一个非空类型(Any),写成 <T : Any>

1
2
3
4
5
6
// 修复:添加 : Any 约束
private fun <T : Any> request(
responseType: ParameterizedTypeReference<T>
): T? {
return restClient.get().uri("...").retrieve().body(responseType)
}