diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java index 6d5e5071b3e..4e8a368c90c 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java @@ -30,6 +30,8 @@ import jakarta.validation.metadata.MethodDescriptor; import jakarta.validation.metadata.MethodType; import jakarta.validation.metadata.ParameterDescriptor; import jakarta.validation.metadata.PropertyDescriptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; @@ -37,6 +39,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.KotlinDetector; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -52,6 +55,8 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP private static final boolean isBeanValidationPresent = ClassUtils.isPresent( "jakarta.validation.Validation", BeanValidationBeanRegistrationAotProcessor.class.getClassLoader()); + private static final Log logger = LogFactory.getLog(BeanValidationBeanRegistrationAotProcessor.class); + @Nullable @Override public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { @@ -67,7 +72,22 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP @Nullable public static BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { - BeanDescriptor descriptor = validator.getConstraintsForClass(registeredBean.getBeanClass()); + BeanDescriptor descriptor; + try { + descriptor = validator.getConstraintsForClass(registeredBean.getBeanClass()); + } + catch (RuntimeException ex) { + if (KotlinDetector.isKotlinType(registeredBean.getBeanClass()) && ex instanceof ArrayIndexOutOfBoundsException) { + // See https://hibernate.atlassian.net/browse/HV-1796 and https://youtrack.jetbrains.com/issue/KT-40857 + logger.warn("Skipping validation constraint hint inference for bean " + registeredBean.getBeanName() + + " due to an ArrayIndexOutOfBoundsException at validator level"); + } + else { + logger.error("Skipping validation constraint hint inference for bean " + + registeredBean.getBeanName(), ex); + } + return null; + } Set> constraintDescriptors = new HashSet<>(); for (MethodDescriptor methodDescriptor : descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)) { for (ParameterDescriptor parameterDescriptor : methodDescriptor.getParameterDescriptors()) { diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java index bb2d5bcddd1..4d28c5ba99b 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java @@ -105,7 +105,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(Exists.List.class) - private @interface Exists { + @interface Exists { String message() default "Does not exist"; @@ -121,7 +121,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { } } - private static class ExistsValidator implements ConstraintValidator { + static class ExistsValidator implements ConstraintValidator { @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { @@ -129,7 +129,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { } } - private static class MethodParameterLevelConstraint { + static class MethodParameterLevelConstraint { @SuppressWarnings("unused") public String hello(@Exists String name) { @@ -139,7 +139,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { } @SuppressWarnings("unused") - private static class ConstructorParameterLevelConstraint { + static class ConstructorParameterLevelConstraint { private final String name; @@ -154,7 +154,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { } @SuppressWarnings("unused") - private static class PropertyLevelConstraint { + static class PropertyLevelConstraint { @Exists private String name; diff --git a/spring-context/src/test/kotlin/org/springframework/validation/beanvalidation/KotlinBeanValidationBeanRegistrationAotProcessorTests.kt b/spring-context/src/test/kotlin/org/springframework/validation/beanvalidation/KotlinBeanValidationBeanRegistrationAotProcessorTests.kt new file mode 100644 index 00000000000..21b26a74d88 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/validation/beanvalidation/KotlinBeanValidationBeanRegistrationAotProcessorTests.kt @@ -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 org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.aot.generate.GenerationContext +import org.springframework.aot.hint.MemberCategory +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates +import org.springframework.aot.test.generate.TestGenerationContext +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RegisteredBean +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.validation.beanvalidation.BeanValidationBeanRegistrationAotProcessorTests.* + +/** + * Kotlin tests for {@link BeanValidationBeanRegistrationAotProcessor}. + * + * @author Sebastien Deleuze + */ +class KotlinBeanValidationBeanRegistrationAotProcessorTests { + + private val processor = BeanValidationBeanRegistrationAotProcessor() + + private val generationContext: GenerationContext = TestGenerationContext() + + @Test + fun shouldProcessMethodParameterLevelConstraint() { + process(MethodParameterLevelConstraint::class.java) + Assertions.assertThat( + RuntimeHintsPredicates.reflection().onType(ExistsValidator::class.java) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) + ).accepts(generationContext.runtimeHints) + } + + @Test + fun shouldSkipMethodParameterLevelConstraintWihExtension() { + process(MethodParameterLevelConstraintWithExtension::class.java) + Assertions.assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty() + } + + private fun process(beanClass: Class<*>) { + val contribution = createContribution(beanClass) + contribution?.applyTo(generationContext, Mockito.mock()) + } + + private fun createContribution(beanClass: Class<*>): BeanRegistrationAotContribution? { + val beanFactory = DefaultListableBeanFactory() + beanFactory.registerBeanDefinition(beanClass.name, RootBeanDefinition(beanClass)) + return processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.name)) + } + + internal class MethodParameterLevelConstraintWithExtension { + + @Suppress("unused") + fun hello(name: @Exists String): String { + return name.toHello() + } + + private fun String.toHello() = + "Hello $this" + } + +}