Skip to content

Commit 357dbc0

Browse files
committed
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
1 parent 525407e commit 357dbc0

File tree

2 files changed

+188
-34
lines changed

2 files changed

+188
-34
lines changed

Diff for: spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java

+112-34
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@
1818

1919
import java.util.Collection;
2020
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Optional;
2124
import java.util.Set;
2225

2326
import jakarta.validation.ConstraintValidator;
2427
import jakarta.validation.NoProviderFoundException;
2528
import jakarta.validation.Validation;
2629
import jakarta.validation.Validator;
30+
import jakarta.validation.ValidatorFactory;
2731
import jakarta.validation.metadata.BeanDescriptor;
2832
import jakarta.validation.metadata.ConstraintDescriptor;
29-
import jakarta.validation.metadata.ConstructorDescriptor;
30-
import jakarta.validation.metadata.MethodDescriptor;
33+
import jakarta.validation.metadata.ContainerElementTypeDescriptor;
34+
import jakarta.validation.metadata.ExecutableDescriptor;
3135
import jakarta.validation.metadata.MethodType;
3236
import jakarta.validation.metadata.ParameterDescriptor;
3337
import jakarta.validation.metadata.PropertyDescriptor;
@@ -36,13 +40,17 @@
3640

3741
import org.springframework.aot.generate.GenerationContext;
3842
import org.springframework.aot.hint.MemberCategory;
43+
import org.springframework.aot.hint.ReflectionHints;
3944
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
4045
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
4146
import org.springframework.beans.factory.aot.BeanRegistrationCode;
4247
import org.springframework.beans.factory.support.RegisteredBean;
4348
import org.springframework.core.KotlinDetector;
49+
import org.springframework.core.ResolvableType;
4450
import org.springframework.lang.Nullable;
51+
import org.springframework.util.Assert;
4552
import org.springframework.util.ClassUtils;
53+
import org.springframework.util.ReflectionUtils;
4654

4755
/**
4856
* AOT {@code BeanRegistrationAotProcessor} that adds additional hints
@@ -80,8 +88,8 @@ private static class BeanValidationDelegate {
8088

8189
@Nullable
8290
private static Validator getValidatorIfAvailable() {
83-
try {
84-
return Validation.buildDefaultValidatorFactory().getValidator();
91+
try (ValidatorFactory validator = Validation.buildDefaultValidatorFactory()) {
92+
return validator.getValidator();
8593
}
8694
catch (NoProviderFoundException ex) {
8795
logger.info("No Bean Validation provider available - skipping validation constraint hint inference");
@@ -95,64 +103,134 @@ public static BeanRegistrationAotContribution processAheadOfTime(RegisteredBean
95103
return null;
96104
}
97105

106+
Class<?> beanClass = registeredBean.getBeanClass();
107+
Set<Class<?>> validatedClasses = new HashSet<>();
108+
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses = new HashSet<>();
109+
110+
processAheadOfTime(beanClass, validatedClasses, constraintValidatorClasses);
111+
112+
if (!validatedClasses.isEmpty() || !constraintValidatorClasses.isEmpty()) {
113+
return new AotContribution(validatedClasses, constraintValidatorClasses);
114+
}
115+
return null;
116+
}
117+
118+
private static void processAheadOfTime(Class<?> clazz, Collection<Class<?>> validatedClasses,
119+
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
120+
121+
Assert.notNull(validator, "Validator can't be null");
122+
98123
BeanDescriptor descriptor;
99124
try {
100-
descriptor = validator.getConstraintsForClass(registeredBean.getBeanClass());
125+
descriptor = validator.getConstraintsForClass(clazz);
101126
}
102127
catch (RuntimeException ex) {
103-
if (KotlinDetector.isKotlinType(registeredBean.getBeanClass()) && ex instanceof ArrayIndexOutOfBoundsException) {
128+
if (KotlinDetector.isKotlinType(clazz) && ex instanceof ArrayIndexOutOfBoundsException) {
104129
// See https://hibernate.atlassian.net/browse/HV-1796 and https://youtrack.jetbrains.com/issue/KT-40857
105-
logger.warn("Skipping validation constraint hint inference for bean " + registeredBean.getBeanName() +
130+
logger.warn("Skipping validation constraint hint inference for class " + clazz +
106131
" due to an ArrayIndexOutOfBoundsException at validator level");
107132
}
108133
else if (ex instanceof TypeNotPresentException) {
109-
logger.debug("Skipping validation constraint hint inference for bean " +
110-
registeredBean.getBeanName() + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
134+
logger.debug("Skipping validation constraint hint inference for class " +
135+
clazz + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
111136
}
112137
else {
113-
logger.warn("Skipping validation constraint hint inference for bean " +
114-
registeredBean.getBeanName(), ex);
138+
logger.warn("Skipping validation constraint hint inference for class " + clazz, ex);
115139
}
116-
return null;
140+
return;
117141
}
118142

119-
Set<ConstraintDescriptor<?>> constraintDescriptors = new HashSet<>();
120-
for (MethodDescriptor methodDescriptor : descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)) {
121-
for (ParameterDescriptor parameterDescriptor : methodDescriptor.getParameterDescriptors()) {
122-
constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors());
123-
}
143+
processExecutableDescriptor(descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER), constraintValidatorClasses);
144+
processExecutableDescriptor(descriptor.getConstrainedConstructors(), constraintValidatorClasses);
145+
processPropertyDescriptors(descriptor.getConstrainedProperties(), constraintValidatorClasses);
146+
if (!constraintValidatorClasses.isEmpty() && shouldProcess(clazz)) {
147+
validatedClasses.add(clazz);
124148
}
125-
for (ConstructorDescriptor constructorDescriptor : descriptor.getConstrainedConstructors()) {
126-
for (ParameterDescriptor parameterDescriptor : constructorDescriptor.getParameterDescriptors()) {
127-
constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors());
149+
150+
ReflectionUtils.doWithFields(clazz, field -> {
151+
Class<?> type = field.getType();
152+
if (Iterable.class.isAssignableFrom(type) || List.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
153+
ResolvableType resolvableType = ResolvableType.forField(field);
154+
Class<?> genericType = resolvableType.getGeneric(0).toClass();
155+
if (shouldProcess(genericType)) {
156+
validatedClasses.add(clazz);
157+
processAheadOfTime(genericType, validatedClasses, constraintValidatorClasses);
158+
}
159+
}
160+
if (Map.class.isAssignableFrom(type)) {
161+
ResolvableType resolvableType = ResolvableType.forField(field);
162+
Class<?> keyGenericType = resolvableType.getGeneric(0).toClass();
163+
Class<?> valueGenericType = resolvableType.getGeneric(1).toClass();
164+
if (shouldProcess(keyGenericType)) {
165+
validatedClasses.add(clazz);
166+
processAheadOfTime(keyGenericType, validatedClasses, constraintValidatorClasses);
167+
}
168+
if (shouldProcess(valueGenericType)) {
169+
validatedClasses.add(clazz);
170+
processAheadOfTime(valueGenericType, validatedClasses, constraintValidatorClasses);
171+
}
172+
}
173+
});
174+
}
175+
176+
private static boolean shouldProcess(Class<?> clazz) {
177+
return !clazz.getCanonicalName().startsWith("java.");
178+
}
179+
180+
private static void processExecutableDescriptor(Set<? extends ExecutableDescriptor> executableDescriptors,
181+
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
182+
183+
for (ExecutableDescriptor executableDescriptor : executableDescriptors) {
184+
for (ParameterDescriptor parameterDescriptor : executableDescriptor.getParameterDescriptors()) {
185+
for (ConstraintDescriptor<?> constraintDescriptor : parameterDescriptor.getConstraintDescriptors()) {
186+
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
187+
}
188+
for (ContainerElementTypeDescriptor typeDescriptor : parameterDescriptor.getConstrainedContainerElementTypes()) {
189+
for (ConstraintDescriptor<?> constraintDescriptor : typeDescriptor.getConstraintDescriptors()) {
190+
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
191+
}
192+
}
128193
}
129194
}
130-
for (PropertyDescriptor propertyDescriptor : descriptor.getConstrainedProperties()) {
131-
constraintDescriptors.addAll(propertyDescriptor.getConstraintDescriptors());
132-
}
133-
if (!constraintDescriptors.isEmpty()) {
134-
return new AotContribution(constraintDescriptors);
195+
}
196+
197+
private static void processPropertyDescriptors(Set<PropertyDescriptor> propertyDescriptors,
198+
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
199+
200+
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
201+
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) {
202+
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
203+
}
204+
for (ContainerElementTypeDescriptor typeDescriptor : propertyDescriptor.getConstrainedContainerElementTypes()) {
205+
for (ConstraintDescriptor<?> constraintDescriptor : typeDescriptor.getConstraintDescriptors()) {
206+
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
207+
}
208+
}
135209
}
136-
return null;
137210
}
138211
}
139212

140213

141214
private static class AotContribution implements BeanRegistrationAotContribution {
142215

143-
private final Collection<ConstraintDescriptor<?>> constraintDescriptors;
216+
private final Collection<Class<?>> validatedClasses;
217+
private final Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses;
144218

145-
public AotContribution(Collection<ConstraintDescriptor<?>> constraintDescriptors) {
146-
this.constraintDescriptors = constraintDescriptors;
219+
public AotContribution(Collection<Class<?>> validatedClasses,
220+
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
221+
222+
this.validatedClasses = validatedClasses;
223+
this.constraintValidatorClasses = constraintValidatorClasses;
147224
}
148225

149226
@Override
150227
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
151-
for (ConstraintDescriptor<?> constraintDescriptor : this.constraintDescriptors) {
152-
for (Class<?> constraintValidatorClass : constraintDescriptor.getConstraintValidatorClasses()) {
153-
generationContext.getRuntimeHints().reflection().registerType(constraintValidatorClass,
154-
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
155-
}
228+
ReflectionHints hints = generationContext.getRuntimeHints().reflection();
229+
for (Class<?> validatedClass : this.validatedClasses) {
230+
hints.registerType(validatedClass, MemberCategory.DECLARED_FIELDS);
231+
}
232+
for (Class<? extends ConstraintValidator<?, ?>> constraintValidatorClass : this.constraintValidatorClasses) {
233+
hints.registerType(constraintValidatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
156234
}
157235
}
158236
}

Diff for: spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java

+76
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@
2020
import java.lang.annotation.Repeatable;
2121
import java.lang.annotation.Retention;
2222
import java.lang.annotation.Target;
23+
import java.util.ArrayList;
24+
import java.util.List;
2325

2426
import jakarta.validation.Constraint;
2527
import jakarta.validation.ConstraintValidator;
2628
import jakarta.validation.ConstraintValidatorContext;
2729
import jakarta.validation.Payload;
30+
import jakarta.validation.Valid;
31+
import jakarta.validation.constraints.Pattern;
32+
import org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator;
2833
import org.junit.jupiter.api.Test;
2934

3035
import org.springframework.aot.generate.GenerationContext;
@@ -67,24 +72,55 @@ void shouldSkipNonAnnotatedType() {
6772
@Test
6873
void shouldProcessMethodParameterLevelConstraint() {
6974
process(MethodParameterLevelConstraint.class);
75+
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
76+
assertThat(RuntimeHintsPredicates.reflection().onType(MethodParameterLevelConstraint.class)
77+
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
7078
assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class)
7179
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
7280
}
7381

7482
@Test
7583
void shouldProcessConstructorParameterLevelConstraint() {
7684
process(ConstructorParameterLevelConstraint.class);
85+
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
86+
assertThat(RuntimeHintsPredicates.reflection().onType(ConstructorParameterLevelConstraint.class)
87+
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
7788
assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class)
7889
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
7990
}
8091

8192
@Test
8293
void shouldProcessPropertyLevelConstraint() {
8394
process(PropertyLevelConstraint.class);
95+
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
96+
assertThat(RuntimeHintsPredicates.reflection().onType(PropertyLevelConstraint.class)
97+
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
8498
assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class)
8599
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
86100
}
87101

102+
@Test
103+
void shouldProcessGenericTypeLevelConstraint() {
104+
process(GenericTypeLevelConstraint.class);
105+
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2);
106+
assertThat(RuntimeHintsPredicates.reflection().onType(GenericTypeLevelConstraint.class)
107+
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
108+
assertThat(RuntimeHintsPredicates.reflection().onType(PatternValidator.class)
109+
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
110+
}
111+
112+
@Test
113+
void shouldProcessTransitiveGenericTypeLevelConstraint() {
114+
process(TransitiveGenericTypeLevelConstraint.class);
115+
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(3);
116+
assertThat(RuntimeHintsPredicates.reflection().onType(TransitiveGenericTypeLevelConstraint.class)
117+
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
118+
assertThat(RuntimeHintsPredicates.reflection().onType(Exclude.class)
119+
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
120+
assertThat(RuntimeHintsPredicates.reflection().onType(PatternValidator.class)
121+
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
122+
}
123+
88124
private void process(Class<?> beanClass) {
89125
BeanRegistrationAotContribution contribution = createContribution(beanClass);
90126
if (contribution != null) {
@@ -168,4 +204,44 @@ public void setName(String name) {
168204
}
169205
}
170206

207+
static class Exclude {
208+
209+
@Valid
210+
private List<@Pattern(regexp="^([1-5][x|X]{2}|[1-5][0-9]{2})\\$") String> httpStatus;
211+
212+
public List<String> getHttpStatus() {
213+
return httpStatus;
214+
}
215+
216+
public void setHttpStatus(List<String> httpStatus) {
217+
this.httpStatus = httpStatus;
218+
}
219+
}
220+
221+
static class GenericTypeLevelConstraint {
222+
223+
private List<@Pattern(regexp="^([1-5][x|X]{2}|[1-5][0-9]{2})\\$") String> httpStatus;
224+
225+
public List<String> getHttpStatus() {
226+
return httpStatus;
227+
}
228+
229+
public void setHttpStatus(List<String> httpStatus) {
230+
this.httpStatus = httpStatus;
231+
}
232+
}
233+
234+
static class TransitiveGenericTypeLevelConstraint {
235+
236+
private List<Exclude> exclude = new ArrayList<>();
237+
238+
public List<Exclude> getExclude() {
239+
return exclude;
240+
}
241+
242+
public void setExclude(List<Exclude> exclude) {
243+
this.exclude = exclude;
244+
}
245+
}
246+
171247
}

0 commit comments

Comments
 (0)