Fix bean validation on suspending function parameters

This commit leverages Hibernate Validator's own internal use
of standard Java reflection to perform validation on suspending
function, which fixes the ArrayIndexOutOfBoundsException previously
observed.

Validation of suspending function return values remains unsupported
as Hibernate Validator is not Coroutines aware.

Closes gh-23499
This commit is contained in:
Sébastien Deleuze 2023-01-31 09:41:47 +01:00
parent 9c6fd3ed06
commit 89c7c6e9dd
3 changed files with 85 additions and 13 deletions

View File

@ -36,6 +36,7 @@ dependencies {
testImplementation("org.apache.commons:commons-pool2")
testImplementation("org.awaitility:awaitility")
testImplementation("jakarta.inject:jakarta.inject-tck")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
testRuntimeOnly("org.glassfish:jakarta.el")
// Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -49,8 +49,6 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.MessageSource;
import org.springframework.core.KotlinDetector;
import org.springframework.core.KotlinReflectionParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
@ -75,6 +73,7 @@ import org.springframework.util.ReflectionUtils;
* {@code jakarta.validation} API being present but no explicit Validator having been configured.
*
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.0
* @see jakarta.validation.ValidatorFactory
* @see jakarta.validation.Validator
@ -118,13 +117,6 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter
private ValidatorFactory validatorFactory;
public LocalValidatorFactoryBean() {
if (KotlinDetector.isKotlinReflectPresent()) {
this.parameterNameDiscoverer = new KotlinReflectionParameterNameDiscoverer();
}
}
/**
* Specify the desired provider class, if any.
* <p>If not specified, JSR-303's default search mechanism will be used.
@ -196,9 +188,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter
/**
* Set the ParameterNameDiscoverer to use for resolving method and constructor
* parameter names if needed for message interpolation.
* <p>Default is Hibernate Validator's own internal use of standard Java reflection,
* with an additional {@link KotlinReflectionParameterNameDiscoverer} if Kotlin
* is present. This may be overridden with a custom subclass or a Spring-controlled
* <p>Default is Hibernate Validator's own internal use of standard Java reflection.
* This may be overridden with a custom subclass or a Spring-controlled
* {@link org.springframework.core.DefaultParameterNameDiscoverer} if necessary.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {

View File

@ -0,0 +1,80 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.validation.beanvalidation
import jakarta.validation.ValidationException
import jakarta.validation.Validator
import jakarta.validation.constraints.NotEmpty
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.aop.framework.ProxyFactory
import org.springframework.validation.annotation.Validated
/**
* Kotlin tests for [MethodValidationInterceptor] + [LocalValidatorFactoryBean].
*
* @author Sebastien Deleuze
*/
@Suppress("UsePropertyAccessSyntax")
class KotlinMethodValidationTests {
@Test
fun parameterValidation() {
val bean = MyValidBean()
val proxyFactory = ProxyFactory(bean)
val validator = LocalValidatorFactoryBean()
validator.afterPropertiesSet()
proxyFactory.addAdvice(MethodValidationInterceptor(validator as Validator))
val proxy = proxyFactory.getProxy() as MyValidBean
Assertions.assertThat(proxy.validName("name")).isEqualTo("name")
Assertions.assertThatExceptionOfType(ValidationException::class.java).isThrownBy {
proxy.validName("")
}
}
@Test
fun coroutinesParameterValidation() = runBlocking<Unit> {
val bean = MyValidCoroutinesBean()
val proxyFactory = ProxyFactory(bean)
val validator = LocalValidatorFactoryBean()
validator.afterPropertiesSet()
proxyFactory.addAdvice(MethodValidationInterceptor(validator as Validator))
val proxy = proxyFactory.getProxy() as MyValidCoroutinesBean
Assertions.assertThat(proxy.validName("name")).isEqualTo("name")
Assertions.assertThatExceptionOfType(ValidationException::class.java).isThrownBy {
runBlocking {
proxy.validName("")
}
}
}
@Validated
open class MyValidBean {
@Suppress("UNUSED_PARAMETER")
open fun validName(@NotEmpty name: String) = name
}
@Validated
open class MyValidCoroutinesBean {
@Suppress("UNUSED_PARAMETER")
open suspend fun validName(@NotEmpty name: String) = name
}
}