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