Spring中Kotlin可空类型反序列化:批量收集空值违规方案咨询
解决方案
针对Kotlin非空属性反序列化时直接抛异常、无法批量收集空值违规的问题,以下是几种更优的实现方案:
方案1:全局配置Jackson跳过非空校验 + Spring Validation统一收集错误
核心思路是先让Jackson允许非空属性接收null(不直接抛异常),再通过Spring校验机制批量检查所有非空约束,最后统一抛出错误。
步骤1:全局配置Jackson关闭严格空检查
由于无法修改库类,通过全局配置让Jackson的Kotlin模块允许非空属性暂时接收null:
@Configuration class JacksonConfig { @Bean fun jackson2ObjectMapperBuilderCustomizer(): Jackson2ObjectMapperBuilderCustomizer { return Jackson2ObjectMapperBuilderCustomizer { builder -> builder.modules(KotlinModule.Builder() .strictNullChecks(false) // 关闭严格空检查,避免反序列化时直接抛异常 .build()) } } }
步骤2:自定义校验器检查Kotlin非空属性
如果库类无法添加@NotNull注解,通过反射检查属性的可空性,批量收集空值违规:
// 自定义校验注解 @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @Constraint(validatedBy = [KotlinNonNullValidator::class]) annotation class KotlinNonNullCheck( val message: String = "存在非空属性为null", val groups: Array<KClass<*>> = [], val payload: Array<KClass<out Payload>> = [] ) // 校验器实现 class KotlinNonNullValidator : ConstraintValidator<KotlinNonNullCheck, Any> { override fun isValid(value: Any?, context: ConstraintValidatorContext): Boolean { if (value == null) return true val violations = mutableListOf<String>() // 遍历所有属性,检查非空约束 value::class.memberProperties.forEach { prop -> val isNullable = prop.returnType.isMarkedNullable val propValue = prop.get(value) if (!isNullable && propValue == null) { violations.add("属性${prop.name}不能为空") // 将错误绑定到校验上下文 context.disableDefaultConstraintViolation() context.buildConstraintViolationWithTemplate("属性${prop.name}不能为空") .addPropertyNode(prop.name) .addConstraintViolation() } } return violations.isEmpty() } }
步骤3:在Controller中触发校验并统一处理错误
通过Validator手动触发校验,或者给类添加@KotlinNonNullCheck注解配合@Valid:
@PostMapping("/test") fun test( @RequestBody request: MyClass1, validator: Validator ) { val violations = validator.validate(request) if (violations.isNotEmpty()) { val errorMsg = violations.joinToString(", ") { "${it.propertyPath}: ${it.message}" } throw InvalidRequestException("请求参数错误:$errorMsg") } // 执行业务逻辑 } // 全局异常处理器统一返回错误格式 @RestControllerAdvice class GlobalExceptionHandler { @ExceptionHandler(ConstraintViolationException::class) fun handleConstraintViolation(e: ConstraintViolationException): ResponseEntity<Map<String, Any>> { val errors = e.constraintViolations.map { "${it.propertyPath}: ${it.message}" } return ResponseEntity.badRequest().body(mapOf("code" to "INVALID_PARAM", "errors" to errors)) } } class InvalidRequestException(message: String) : RuntimeException(message)
方案2:用MapStruct自动生成可空DTO并转换
如果不想手动维护副本类,用MapStruct自动生成属性全可空的DTO,校验完成后再转换为原始库类:
步骤1:添加MapStruct依赖
// build.gradle.kts dependencies { implementation("org.mapstruct:mapstruct:1.5.5.Final") kapt("org.mapstruct:mapstruct-processor:1.5.5.Final") }
步骤2:定义DTO和映射接口
// 可空DTO data class MyClass1Dto( val someField1: String?, val myClass2: MyClass2Dto?, ) data class MyClass2Dto( val someField2: String?, val someField3: String?, ) // MapStruct自动生成映射实现 @Mapper(componentModel = "spring") interface MyClassMapper { fun toMyClass1(dto: MyClass1Dto): MyClass1 fun toMyClass2(dto: MyClass2Dto): MyClass2 }
步骤3:Controller中校验DTO并转换
@PostMapping("/test") fun test( @RequestBody dto: MyClass1Dto, mapper: MyClassMapper ) { val violations = mutableListOf<String>() // 批量检查非空约束 if (dto.someField1 == null) violations.add("someField1不能为空") dto.myClass2?.let { if (it.someField2 == null) violations.add("myClass2.someField2不能为空") } if (violations.isNotEmpty()) { throw InvalidRequestException(violations.joinToString(", ")) } // 转换为原始库类 val request = mapper.toMyClass1(dto) // 执行业务逻辑 }
方案3:修复自定义Deserializer的循环问题
针对之前遇到的反序列化循环问题,改用Jackson底层API解析JSON树,再委托默认反序列化器创建对象:
class CollectingDeserializer<T>(private val targetType: Class<T>) : JsonDeserializer<T>() { private val delegateDeserializer by lazy { val factory = BeanDeserializerFactory.instance val config = deserializationConfig val beanDesc = config.introspect(JavaType.constructType(targetType)) factory.buildBeanDeserializer(config, beanDesc, null) } override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T? { val violations = mutableListOf<String>() // 先解析为JSON树,避免循环调用 val jsonNode = p.codec.readTree<JsonNode>(p) // 遍历字段检查非空约束 targetType.declaredFields.forEach { field -> field.isAccessible = true val fieldName = field.name val node = jsonNode.get(fieldName) val isNullable = field.type.kotlin.isMarkedNullable if (!isNullable && (node == null || node.isNull)) { violations.add("字段$fieldName不能为空") } } if (violations.isNotEmpty()) { throw InvalidRequestException(violations.joinToString(", ")) } // 重置解析器位置,用委托反序列化器创建对象 p.codec.treeToValue(jsonNode, targetType) return delegateDeserializer.deserialize(p, ctxt) } }
全局注册反序列化器:
@Configuration class JacksonConfig { @Bean fun jackson2ObjectMapperBuilderCustomizer(): Jackson2ObjectMapperBuilderCustomizer { return Jackson2ObjectMapperBuilderCustomizer { builder -> builder.deserializerByType(MyClass1::class.java, CollectingDeserializer(MyClass1::class.java)) builder.deserializerByType(MyClass2::class.java, CollectingDeserializer(MyClass2::class.java)) } } }
内容的提问来源于stack exchange,提问作者Elektor
相关产品推荐
相关产品推荐

