无法验证 Spring Boot/Kotlin 协程控制器中的请求参数

发布于 2025-01-12 01:53:43 字数 780 浏览 0 评论 0 原文

在 SpringBoot/Kotlin 协程项目中,我有一个像这样的控制器类。


@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 一样工作,但是在测试

验证请求参数时,它失败并抛出异常,例如:

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 。

In a SpringBoot/Kotlin Coroutines project, I have a controller class like this.


@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> {}

}

The validation on the @ResquestBody works as the general Spring WebFlux, but when testing

validating request params , it failed and throws an exception like:

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:

It is not a ConstraintViolationException.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

顾铮苏瑾 2025-01-19 01:53:43

我认为当您使用协程时,这是框架中的一个错误(更新,是的,我看到快乐歌曲评论)。总之:

“@Validated 确实还不符合协程标准,我们需要通过使用协程感知方法来发现方法参数来解决这个问题。”

麻烦的是,你的控制器上的方法的签名实际上被Spring增强了,有一个额外的参数,像这样,添加一个延续:

public java.lang.Object com.example.react.PostController.search(java.lang.String,int,int,kotlin.coroutines.Continuation)

所以当hibernate验证器调用getParameter名称来获取你的方法上的参数列表时,它认为请求中总共有 4 个,然后尝试获取第 4 个(索引 3)时出现索引越界异常。

如果你在 this: 的返回上放置一个断点

   @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/

I think this is a bug in the framework when you are using coroutines (update , it is, I saw Happy Songs comment). In summary:

"@Validated is indeed not yet Coroutines compliant, we need to fix that by using Coroutines aware methods to discover method parameters."

The trouble is that the signature of the method on your controller is actually enhanced by Spring to have an extra parameter, like this, adding a continuation:

public java.lang.Object com.example.react.PostController.search(java.lang.String,int,int,kotlin.coroutines.Continuation)

so when the hibernate validator calls getParameter names to get the list of parameters on your method, it thinks there are 4 in total on the request, and then gets an index out of bounds exception trying to get the 4th (index 3).

If you put a breakpoint on the return of this:

   @Override
        public E get(int index) {
            return a[index];
        }

and put a breakpoint condition of index ==3 && a.length <4 you can see what is going on.

I'd report it as a bug on the Spring issue tracker.

You might be better off taking an alternative approach, as described here, using a RequestBody as a DTO and using the @Valid annotation

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

疑心病 2025-01-19 01:53:43

感谢快乐歌曲的评论,我现在找到了克服这个障碍的最佳解决方案 Spring Github 问题#23499

正如此问题的评论和 PaulNuk 的答案中所解释的,运行时将在方法参数中附加一个 Continuation ,这将使 Hibernate 验证器中方法参数名称的索引计算失败。

解决方案是更改 ParameterNameDiscoverer.getParameterNames(Method) 方法,并在它是 挂起 函数时添加一个空字符串作为附加参数名称。

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 获取完整示例代码

更新:Spring 6 内置了对 Kotlin 协程控制器的验证支持。

Thanks for the happy songs' comments, I found the best solution by now to overcome this barrier from the Spring Github issues#23499.

As explained in comments of this issue and PaulNuk's answer, there is a Continuation will be appended to the method arguments at runtime, which will fail the index computation of the method parameter names in the Hibernate Validator.

The solution is changing the ParameterNameDiscoverer.getParameterNames(Method) method and adding a empty string as the additional parameter name when it is a suspend function.

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
    }
}

Then declare a new validator factory bean.

    @Primary
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun defaultValidator(): LocalValidatorFactoryBean {
        val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean()
        factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject()
        return factoryBean
    }

Get the complete sample codes from my Github.

Update: Spring 6 has built-in Validation Support for Kotlin Coroutines Controller.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文