无法验证 Spring Boot/Kotlin 协程控制器中的请求参数
failed to validate request params in a Spring Boot/Kotlin Coroutines controller
在 SpringBoot/Kotlin 协程项目中,我有一个这样的控制器 class。
@RestContollser
@Validated
class PostController(private val posts: PostRepository) {
suspend fun search(@RequestParam q:String, @RequestParam @Min(0) offset:Int, @RequestParam @Min(1) limit:Int): ResponseEntity<Any> {}
}
@ResquestBody
上的验证与一般 Spring WebFlux 一样,但在测试时
validating request params ,它失败并抛出如下异常:
java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:
这不是 ConstraintViolationException
。
我认为这是在使用协程时框架中的一个错误(更新,是的,我看到快乐歌曲评论)。总结:
“@Validated 确实还不兼容协程,我们需要通过使用协程感知方法来发现方法参数来解决这个问题。”
问题是你的控制器上的方法签名实际上被 Spring 增强了一个额外的参数,像这样,添加一个延续:
public java.lang.Object com.example.react.PostController.search(java.lang.String,int,int,kotlin.coroutines.Continuation)
所以当 hibernate 验证器调用 getParameter names 来获取你的方法的参数列表时,它认为请求中总共有 4 个,然后获取一个索引越界异常试图获取第 4 个(索引3).
如果你在return处设置断点:
@Override
public E get(int index) {
return a[index];
}
并设置断点条件index ==3 && a.length <4
你可以看到发生了什么。
我会在 Spring 问题跟踪器上将其报告为错误。
您最好采用替代方法,如此处所述,使用 RequestBody 作为 DTO 并使用 @Valid 注释
感谢快乐歌曲的评论,我现在找到了克服这个障碍的最佳解决方案Spring Github issues#23499。
正如这个问题的评论和 PaulNuk 的回答中所解释的,有一个 Continuation
将在运行时附加到方法参数,这将使 Hibernate Validator 中的方法参数名称的索引计算失败。
解决方法是改变ParameterNameDiscoverer.getParameterNames(Method)
方法,当它是suspend函数时,添加一个空字符串作为附加参数名称。
class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() {
override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE
override fun postProcessConfiguration(configuration: javax.validation.Configuration<*>) {
super.postProcessConfiguration(configuration)
val discoverer = PrioritizedParameterNameDiscoverer()
discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer())
discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer())
discoverer.addDiscoverer(LocalVariableTableParameterNameDiscoverer())
val defaultProvider = configuration.defaultParameterNameProvider
configuration.parameterNameProvider(object : ParameterNameProvider {
override fun getParameterNames(constructor: Constructor<*>): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(constructor)
return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor)
}
override fun getParameterNames(method: Method): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(method)
return paramNames?.toList() ?: defaultProvider.getParameterNames(method)
}
})
}
}
class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer {
private val defaultProvider = KotlinReflectionParameterNameDiscoverer()
override fun getParameterNames(constructor: Constructor<*>): Array<String>? =
defaultProvider.getParameterNames(constructor)
override fun getParameterNames(method: Method): Array<String>? {
val defaultNames = defaultProvider.getParameterNames(method) ?: return null
val function = method.kotlinFunction
return if (function != null && function.isSuspend) {
defaultNames + ""
} else defaultNames
}
}
然后声明一个新的验证器工厂 bean。
@Primary
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun defaultValidator(): LocalValidatorFactoryBean {
val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean()
factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject()
return factoryBean
}
从我的 Github 中获取 complete sample codes。
在 SpringBoot/Kotlin 协程项目中,我有一个这样的控制器 class。
@RestContollser
@Validated
class PostController(private val posts: PostRepository) {
suspend fun search(@RequestParam q:String, @RequestParam @Min(0) offset:Int, @RequestParam @Min(1) limit:Int): ResponseEntity<Any> {}
}
@ResquestBody
上的验证与一般 Spring WebFlux 一样,但在测试时
validating request params ,它失败并抛出如下异常:
java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below:
这不是 ConstraintViolationException
。
我认为这是在使用协程时框架中的一个错误(更新,是的,我看到快乐歌曲评论)。总结:
“@Validated 确实还不兼容协程,我们需要通过使用协程感知方法来发现方法参数来解决这个问题。”
问题是你的控制器上的方法签名实际上被 Spring 增强了一个额外的参数,像这样,添加一个延续:
public java.lang.Object com.example.react.PostController.search(java.lang.String,int,int,kotlin.coroutines.Continuation)
所以当 hibernate 验证器调用 getParameter names 来获取你的方法的参数列表时,它认为请求中总共有 4 个,然后获取一个索引越界异常试图获取第 4 个(索引3).
如果你在return处设置断点:
@Override
public E get(int index) {
return a[index];
}
并设置断点条件index ==3 && a.length <4
你可以看到发生了什么。
我会在 Spring 问题跟踪器上将其报告为错误。
您最好采用替代方法,如此处所述,使用 RequestBody 作为 DTO 并使用 @Valid 注释
感谢快乐歌曲的评论,我现在找到了克服这个障碍的最佳解决方案Spring Github issues#23499。
正如这个问题的评论和 PaulNuk 的回答中所解释的,有一个 Continuation
将在运行时附加到方法参数,这将使 Hibernate Validator 中的方法参数名称的索引计算失败。
解决方法是改变ParameterNameDiscoverer.getParameterNames(Method)
方法,当它是suspend函数时,添加一个空字符串作为附加参数名称。
class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() {
override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE
override fun postProcessConfiguration(configuration: javax.validation.Configuration<*>) {
super.postProcessConfiguration(configuration)
val discoverer = PrioritizedParameterNameDiscoverer()
discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer())
discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer())
discoverer.addDiscoverer(LocalVariableTableParameterNameDiscoverer())
val defaultProvider = configuration.defaultParameterNameProvider
configuration.parameterNameProvider(object : ParameterNameProvider {
override fun getParameterNames(constructor: Constructor<*>): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(constructor)
return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor)
}
override fun getParameterNames(method: Method): List<String> {
val paramNames: Array<String>? = discoverer.getParameterNames(method)
return paramNames?.toList() ?: defaultProvider.getParameterNames(method)
}
})
}
}
class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer {
private val defaultProvider = KotlinReflectionParameterNameDiscoverer()
override fun getParameterNames(constructor: Constructor<*>): Array<String>? =
defaultProvider.getParameterNames(constructor)
override fun getParameterNames(method: Method): Array<String>? {
val defaultNames = defaultProvider.getParameterNames(method) ?: return null
val function = method.kotlinFunction
return if (function != null && function.isSuspend) {
defaultNames + ""
} else defaultNames
}
}
然后声明一个新的验证器工厂 bean。
@Primary
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun defaultValidator(): LocalValidatorFactoryBean {
val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean()
factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject()
return factoryBean
}
从我的 Github 中获取 complete sample codes。