Add AOT support for container element constraints

This commit introduces support for bean validation container
element constraints, including transitive ones.

Transitive constraints in the parameterized types of a container
are not discoverable via the BeanDescriptor, so a complementary
type discovery is done on Spring side to cover the related use
case.

Closes gh-33842
This commit is contained in:
Sébastien Deleuze 2024-11-13 10:30:57 +01:00
parent 525407e4b1
commit 357dbc0354
2 changed files with 188 additions and 34 deletions

View File

@ -18,16 +18,20 @@ package org.springframework.validation.beanvalidation;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.NoProviderFoundException;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.metadata.BeanDescriptor;
import jakarta.validation.metadata.ConstraintDescriptor;
import jakarta.validation.metadata.ConstructorDescriptor;
import jakarta.validation.metadata.MethodDescriptor;
import jakarta.validation.metadata.ContainerElementTypeDescriptor;
import jakarta.validation.metadata.ExecutableDescriptor;
import jakarta.validation.metadata.MethodType;
import jakarta.validation.metadata.ParameterDescriptor;
import jakarta.validation.metadata.PropertyDescriptor;
@ -36,13 +40,17 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
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.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* AOT {@code BeanRegistrationAotProcessor} that adds additional hints
@ -80,8 +88,8 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
@Nullable
private static Validator getValidatorIfAvailable() {
try {
return Validation.buildDefaultValidatorFactory().getValidator();
try (ValidatorFactory validator = Validation.buildDefaultValidatorFactory()) {
return validator.getValidator();
}
catch (NoProviderFoundException ex) {
logger.info("No Bean Validation provider available - skipping validation constraint hint inference");
@ -95,64 +103,134 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
return null;
}
Class<?> beanClass = registeredBean.getBeanClass();
Set<Class<?>> validatedClasses = new HashSet<>();
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses = new HashSet<>();
processAheadOfTime(beanClass, validatedClasses, constraintValidatorClasses);
if (!validatedClasses.isEmpty() || !constraintValidatorClasses.isEmpty()) {
return new AotContribution(validatedClasses, constraintValidatorClasses);
}
return null;
}
private static void processAheadOfTime(Class<?> clazz, Collection<Class<?>> validatedClasses,
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
Assert.notNull(validator, "Validator can't be null");
BeanDescriptor descriptor;
try {
descriptor = validator.getConstraintsForClass(registeredBean.getBeanClass());
descriptor = validator.getConstraintsForClass(clazz);
}
catch (RuntimeException ex) {
if (KotlinDetector.isKotlinType(registeredBean.getBeanClass()) && ex instanceof ArrayIndexOutOfBoundsException) {
if (KotlinDetector.isKotlinType(clazz) && 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() +
logger.warn("Skipping validation constraint hint inference for class " + clazz +
" due to an ArrayIndexOutOfBoundsException at validator level");
}
else if (ex instanceof TypeNotPresentException) {
logger.debug("Skipping validation constraint hint inference for bean " +
registeredBean.getBeanName() + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
logger.debug("Skipping validation constraint hint inference for class " +
clazz + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
}
else {
logger.warn("Skipping validation constraint hint inference for bean " +
registeredBean.getBeanName(), ex);
logger.warn("Skipping validation constraint hint inference for class " + clazz, ex);
}
return null;
return;
}
Set<ConstraintDescriptor<?>> constraintDescriptors = new HashSet<>();
for (MethodDescriptor methodDescriptor : descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)) {
for (ParameterDescriptor parameterDescriptor : methodDescriptor.getParameterDescriptors()) {
constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors());
processExecutableDescriptor(descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER), constraintValidatorClasses);
processExecutableDescriptor(descriptor.getConstrainedConstructors(), constraintValidatorClasses);
processPropertyDescriptors(descriptor.getConstrainedProperties(), constraintValidatorClasses);
if (!constraintValidatorClasses.isEmpty() && shouldProcess(clazz)) {
validatedClasses.add(clazz);
}
ReflectionUtils.doWithFields(clazz, field -> {
Class<?> type = field.getType();
if (Iterable.class.isAssignableFrom(type) || List.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
ResolvableType resolvableType = ResolvableType.forField(field);
Class<?> genericType = resolvableType.getGeneric(0).toClass();
if (shouldProcess(genericType)) {
validatedClasses.add(clazz);
processAheadOfTime(genericType, validatedClasses, constraintValidatorClasses);
}
}
if (Map.class.isAssignableFrom(type)) {
ResolvableType resolvableType = ResolvableType.forField(field);
Class<?> keyGenericType = resolvableType.getGeneric(0).toClass();
Class<?> valueGenericType = resolvableType.getGeneric(1).toClass();
if (shouldProcess(keyGenericType)) {
validatedClasses.add(clazz);
processAheadOfTime(keyGenericType, validatedClasses, constraintValidatorClasses);
}
if (shouldProcess(valueGenericType)) {
validatedClasses.add(clazz);
processAheadOfTime(valueGenericType, validatedClasses, constraintValidatorClasses);
}
}
});
}
private static boolean shouldProcess(Class<?> clazz) {
return !clazz.getCanonicalName().startsWith("java.");
}
private static void processExecutableDescriptor(Set<? extends ExecutableDescriptor> executableDescriptors,
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
for (ExecutableDescriptor executableDescriptor : executableDescriptors) {
for (ParameterDescriptor parameterDescriptor : executableDescriptor.getParameterDescriptors()) {
for (ConstraintDescriptor<?> constraintDescriptor : parameterDescriptor.getConstraintDescriptors()) {
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
}
for (ContainerElementTypeDescriptor typeDescriptor : parameterDescriptor.getConstrainedContainerElementTypes()) {
for (ConstraintDescriptor<?> constraintDescriptor : typeDescriptor.getConstraintDescriptors()) {
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
}
}
}
}
for (ConstructorDescriptor constructorDescriptor : descriptor.getConstrainedConstructors()) {
for (ParameterDescriptor parameterDescriptor : constructorDescriptor.getParameterDescriptors()) {
constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors());
}
private static void processPropertyDescriptors(Set<PropertyDescriptor> propertyDescriptors,
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) {
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
}
for (ContainerElementTypeDescriptor typeDescriptor : propertyDescriptor.getConstrainedContainerElementTypes()) {
for (ConstraintDescriptor<?> constraintDescriptor : typeDescriptor.getConstraintDescriptors()) {
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
}
}
}
for (PropertyDescriptor propertyDescriptor : descriptor.getConstrainedProperties()) {
constraintDescriptors.addAll(propertyDescriptor.getConstraintDescriptors());
}
if (!constraintDescriptors.isEmpty()) {
return new AotContribution(constraintDescriptors);
}
return null;
}
}
private static class AotContribution implements BeanRegistrationAotContribution {
private final Collection<ConstraintDescriptor<?>> constraintDescriptors;
private final Collection<Class<?>> validatedClasses;
private final Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses;
public AotContribution(Collection<ConstraintDescriptor<?>> constraintDescriptors) {
this.constraintDescriptors = constraintDescriptors;
public AotContribution(Collection<Class<?>> validatedClasses,
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
this.validatedClasses = validatedClasses;
this.constraintValidatorClasses = constraintValidatorClasses;
}
@Override
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
for (ConstraintDescriptor<?> constraintDescriptor : this.constraintDescriptors) {
for (Class<?> constraintValidatorClass : constraintDescriptor.getConstraintValidatorClasses()) {
generationContext.getRuntimeHints().reflection().registerType(constraintValidatorClass,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
ReflectionHints hints = generationContext.getRuntimeHints().reflection();
for (Class<?> validatedClass : this.validatedClasses) {
hints.registerType(validatedClass, MemberCategory.DECLARED_FIELDS);
}
for (Class<? extends ConstraintValidator<?, ?>> constraintValidatorClass : this.constraintValidatorClasses) {
hints.registerType(constraintValidatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}
}

View File

@ -20,11 +20,16 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GenerationContext;
@ -67,6 +72,9 @@ class BeanValidationBeanRegistrationAotProcessorTests {
@Test
void shouldProcessMethodParameterLevelConstraint() {
process(MethodParameterLevelConstraint.class);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
assertThat(RuntimeHintsPredicates.reflection().onType(MethodParameterLevelConstraint.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
}
@ -74,6 +82,9 @@ class BeanValidationBeanRegistrationAotProcessorTests {
@Test
void shouldProcessConstructorParameterLevelConstraint() {
process(ConstructorParameterLevelConstraint.class);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
assertThat(RuntimeHintsPredicates.reflection().onType(ConstructorParameterLevelConstraint.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
}
@ -81,10 +92,35 @@ class BeanValidationBeanRegistrationAotProcessorTests {
@Test
void shouldProcessPropertyLevelConstraint() {
process(PropertyLevelConstraint.class);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
assertThat(RuntimeHintsPredicates.reflection().onType(PropertyLevelConstraint.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
}
@Test
void shouldProcessGenericTypeLevelConstraint() {
process(GenericTypeLevelConstraint.class);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
assertThat(RuntimeHintsPredicates.reflection().onType(GenericTypeLevelConstraint.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
assertThat(RuntimeHintsPredicates.reflection().onType(PatternValidator.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
}
@Test
void shouldProcessTransitiveGenericTypeLevelConstraint() {
process(TransitiveGenericTypeLevelConstraint.class);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(3);
assertThat(RuntimeHintsPredicates.reflection().onType(TransitiveGenericTypeLevelConstraint.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
assertThat(RuntimeHintsPredicates.reflection().onType(Exclude.class)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
assertThat(RuntimeHintsPredicates.reflection().onType(PatternValidator.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
}
private void process(Class<?> beanClass) {
BeanRegistrationAotContribution contribution = createContribution(beanClass);
if (contribution != null) {
@ -168,4 +204,44 @@ class BeanValidationBeanRegistrationAotProcessorTests {
}
}
static class Exclude {
@Valid
private List<@Pattern(regexp="^([1-5][x|X]{2}|[1-5][0-9]{2})\\$") String> httpStatus;
public List<String> getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(List<String> httpStatus) {
this.httpStatus = httpStatus;
}
}
static class GenericTypeLevelConstraint {
private List<@Pattern(regexp="^([1-5][x|X]{2}|[1-5][0-9]{2})\\$") String> httpStatus;
public List<String> getHttpStatus() {
return httpStatus;
}
public void setHttpStatus(List<String> httpStatus) {
this.httpStatus = httpStatus;
}
}
static class TransitiveGenericTypeLevelConstraint {
private List<Exclude> exclude = new ArrayList<>();
public List<Exclude> getExclude() {
return exclude;
}
public void setExclude(List<Exclude> exclude) {
this.exclude = exclude;
}
}
}