无法验证 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 注释

https://www.vinsguru.com/spring-webflux-validation/

感谢快乐歌曲的评论,我现在找到了克服这个障碍的最佳解决方案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