Kotlin与Spring Boot中非空校验失效的解决方案
最近在项目中遇到了一个令人困惑的问题,涉及到Kotlin的非空类型和Spring Boot的校验注解。在处理JSON请求时,我发现Kotlin的非空类型和JSR380的@NotNull注解都没有如预期那样生效。这让我深入探索了Kotlin与Spring Boot在反序列化和校验过程中的一些细节。
问题场景
我有一个SignInDTO数据类,用于处理用户的签到请求。代码如下:
1 | class SignInDTO( |
在控制器中,我使用了@Validated和@RequestBody注解来接收和校验请求体:
1 |
|
当我发送一个缺少projectId或coordinate字段的JSON请求时,预期应该抛出校验异常。然而,projectId没有抛出异常,而是给了一个0的默认值。coordinate字段却能正常校验缺失。这让我感到疑惑。
补充:预期的情况下,
@NotNull是冗余的,因为如果存在null值,DTO对象在实例化的过程中就会抛出异常。而JSR380的校验则是发生在传入Controller方法的阶段。
原因分析
经过调试和查阅资料,我发现问题的根源在于Kotlin的非空基本类型和Java的基本类型之间的映射。
- Kotlin的非空基本类型(如
Int)会被编译成Java的基本类型(如int)。在Java中,基本类型无法为null,如果JSON中缺少对应的字段,反序列化时会使用默认值(0)进行填充,这导致JSR380中@NotNull注解同时也失效了。 coordinate字段是一个对象类型(Point),在Kotlin中,它被处理为非空的引用类型。如果在JSON中缺少该字段,反序列化时会抛出MissingKotlinParameterException,这正是我们期望的行为。
因此,projectId字段即使在请求中缺失,也会被默认为0,导致@NotNull或@NotBlank等注解无法生效。
解决方案
知道了问题的原因,解决方法也就清晰了。主要有两种思路:
将基本类型改为可空类型
将Int改为Int?,使其在Kotlin中编译为包装类型Integer,从而可以为null。这样,@NotNull注解就能在反序列化时正常校验。
1 | class SignInDTO( |
缺点:这会在代码中引入不必要的空值检查,因为在设计上projectId确实是非null的。
使用@Min或@Max注解校验默认值
利用@Min(1)注解,确保projectId的值必须大于等于1。由于默认值为0,当请求中缺少projectId时,校验将无法通过。
1 | class SignInDTO( |
优点:无需更改类型,也不引入额外的空值检查。
缺点:使用@Min或@Max判空是违反直觉的。
完善全局异常处理
同时,为了捕获对象类型字段缺失时抛出的MissingKotlinParameterException,我还完善了全局异常处理器:
1 |
|
这样,当对象类型字段缺失时,程序会返回明确的错误提示,指明具体是哪个字段缺失。
顺便贴上应对JSR380校验失败时抛出MethodArgumentNotValidException的异常处理器:
1 |
|
思考
Kotlin中的基本类型,如Int和Int?在编译时分别对应Java中的基本类型int和包装类型Integer,这在性能和空值安全性之间做了一个权衡。
- 性能考量:基本类型
int的设计初衷是为了更高效的内存使用和性能优化。Java虚拟机处理基本类型时,不需要进行额外的装箱和拆箱操作,这在大量数据处理或性能敏感的场景下显得尤为重要。因此,Kotlin将非空的Int直接编译为int,继承了Java的这一优势。对于不涉及空值的场景,这种设计是合理且高效的。 - 空值安全:然而,Kotlin的设计哲学之一是空值安全,即通过类型系统在编译期尽可能地消除空指针异常(NullPointerException)。因此,Kotlin提供了可空类型
Int?,对于可空类型,Kotlin强制要求开发者显式地处理 null 值,从而在编译时捕获潜在的空引用错误,避免了 Java 中常见的 NullPointerException 问题。 - 设计权衡:这一设计在性能和安全性之间做了权衡。虽然
Int的非空设计在大多数情况下保障了性能,但在特定场景下(如反序列化),它可能会导致意外的行为,比如我们遇到的反序列化时默认值为0的问题。在这些场景下,编译后的int由于不能为null,反而打破了我们对数据校验的期望。相比之下,Int?虽然增加了空值处理的复杂性,但允许更灵活的空值校验。
在StackOverflow上,我发现了一个类似的问题,并将我的解决方案分享了上去:Kotlin + Spring Boot default value for Int field from JSON