Browse Source

Catch defensively validator exceptions in AOT processing

An ArrayIndexOutOfBoundsException is thrown by
Validator.getConstraintsForClass when processing Kotlin beans
with extensions functions (Kotlin or Hibernate Validator bug).

This commit catches those exceptions and report them as warning
without the full stactrace, and report as well other
ones thrown as errors with the full stracktrace.

Closes gh-30037
pull/30047/head
Sébastien Deleuze 3 years ago
parent
commit
79f43041ad
  1. 22
      spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java
  2. 10
      spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java
  3. 80
      spring-context/src/test/kotlin/org/springframework/validation/beanvalidation/KotlinBeanValidationBeanRegistrationAotProcessorTests.kt

22
spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java

@ -30,6 +30,8 @@ import jakarta.validation.metadata.MethodDescriptor; @@ -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; @@ -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 @@ -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 @@ -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<ConstraintDescriptor<?>> constraintDescriptors = new HashSet<>();
for (MethodDescriptor methodDescriptor : descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)) {
for (ParameterDescriptor parameterDescriptor : methodDescriptor.getParameterDescriptors()) {

10
spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java

@ -105,7 +105,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { @@ -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 { @@ -121,7 +121,7 @@ class BeanValidationBeanRegistrationAotProcessorTests {
}
}
private static class ExistsValidator implements ConstraintValidator<Exists, String> {
static class ExistsValidator implements ConstraintValidator<Exists, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
@ -129,7 +129,7 @@ class BeanValidationBeanRegistrationAotProcessorTests { @@ -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 { @@ -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 { @@ -154,7 +154,7 @@ class BeanValidationBeanRegistrationAotProcessorTests {
}
@SuppressWarnings("unused")
private static class PropertyLevelConstraint {
static class PropertyLevelConstraint {
@Exists
private String name;

80
spring-context/src/test/kotlin/org/springframework/validation/beanvalidation/KotlinBeanValidationBeanRegistrationAotProcessorTests.kt

@ -0,0 +1,80 @@ @@ -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"
}
}
Loading…
Cancel
Save