Skip to content

Commit

Permalink
Catch defensively validator exceptions in AOT processing
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sdeleuze committed Feb 27, 2023
1 parent edb4a34 commit 79f4304
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@
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;
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;

Expand All @@ -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) {
Expand All @@ -67,7 +72,22 @@ private static class BeanValidationDelegate {

@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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static class EmptyClass { }
@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";

Expand All @@ -121,15 +121,15 @@ private static class EmptyClass { }
}
}

private static class ExistsValidator implements ConstraintValidator<Exists, String> {
static class ExistsValidator implements ConstraintValidator<Exists, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
return true;
}
}

private static class MethodParameterLevelConstraint {
static class MethodParameterLevelConstraint {

@SuppressWarnings("unused")
public String hello(@Exists String name) {
Expand All @@ -139,7 +139,7 @@ public String hello(@Exists String name) {
}

@SuppressWarnings("unused")
private static class ConstructorParameterLevelConstraint {
static class ConstructorParameterLevelConstraint {

private final String name;

Expand All @@ -154,7 +154,7 @@ public String hello() {
}

@SuppressWarnings("unused")
private static class PropertyLevelConstraint {
static class PropertyLevelConstraint {

@Exists
private String name;
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}

}

0 comments on commit 79f4304

Please sign in to comment.