Skip to content

Commit f60f3cb

Browse files
wilkinsonaphilwebb
authored andcommitted
Exclude property beans from method validation
Exclude `@ConfigurationProperties` beans from method validation so that `@Validated` can be used on final classes without the method validation post-processor throwing an exception. This commit introduces a `FilteredMethodValidationPostProcessor` class which will use `MethodValidationExcludeFilters` to exclude beans from method validation processing. Using `@EnableConfigurationProperties` will automatically register an appropriate filter. Closes gh-21454
1 parent a0862f9 commit f60f3cb

File tree

9 files changed

+288
-7
lines changed

9 files changed

+288
-7
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,12 +19,15 @@
1919
import javax.validation.Validator;
2020
import javax.validation.executable.ExecutableValidator;
2121

22+
import org.springframework.beans.factory.ObjectProvider;
2223
import org.springframework.beans.factory.config.BeanDefinition;
2324
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2425
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2526
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2627
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
2728
import org.springframework.boot.validation.MessageInterpolatorFactory;
29+
import org.springframework.boot.validation.beanvalidation.FilteredMethodValidationPostProcessor;
30+
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
2831
import org.springframework.context.annotation.Bean;
2932
import org.springframework.context.annotation.Configuration;
3033
import org.springframework.context.annotation.Import;
@@ -61,8 +64,9 @@ public static LocalValidatorFactoryBean defaultValidator() {
6164
@Bean
6265
@ConditionalOnMissingBean
6366
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
64-
@Lazy Validator validator) {
65-
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
67+
@Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
68+
FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
69+
excludeFilters.orderedStream());
6670
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
6771
processor.setProxyTargetClass(proxyTargetClass);
6872
processor.setValidator(validator);

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.beans.factory.config.BeanPostProcessor;
3131
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigurationTests.CustomValidatorConfiguration.TestBeanPostProcessor;
3232
import org.springframework.boot.test.util.TestPropertyValues;
33+
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
3334
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3435
import org.springframework.context.annotation.Bean;
3536
import org.springframework.context.annotation.Configuration;
@@ -43,6 +44,7 @@
4344

4445
import static org.assertj.core.api.Assertions.assertThat;
4546
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
47+
import static org.assertj.core.api.Assertions.assertThatNoException;
4648
import static org.mockito.Mockito.mock;
4749

4850
/**
@@ -159,6 +161,15 @@ void validationIsEnabled() {
159161
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething("KO"));
160162
}
161163

164+
@Test
165+
void classCanBeExcludedFromValidation() {
166+
load(ExcludedServiceConfiguration.class);
167+
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1);
168+
ExcludedService service = this.context.getBean(ExcludedService.class);
169+
service.doSomething("Valid");
170+
assertThatNoException().isThrownBy(() -> service.doSomething("KO"));
171+
}
172+
162173
@Test
163174
void validationUsesCglibProxy() {
164175
load(DefaultAnotherSampleService.class);
@@ -285,6 +296,29 @@ void doSomething(@Size(min = 3, max = 10) String name) {
285296

286297
}
287298

299+
@Configuration(proxyBeanMethods = false)
300+
static final class ExcludedServiceConfiguration {
301+
302+
@Bean
303+
ExcludedService excludedService() {
304+
return new ExcludedService();
305+
}
306+
307+
@Bean
308+
MethodValidationExcludeFilter exclusionFilter() {
309+
return (type) -> type.equals(ExcludedService.class);
310+
}
311+
312+
}
313+
314+
@Validated
315+
static final class ExcludedService {
316+
317+
void doSomething(@Size(min = 3, max = 10) String name) {
318+
}
319+
320+
}
321+
288322
interface AnotherSampleService {
289323

290324
void doSomething(@Min(42) Integer counter);

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
import java.util.Set;
2121
import java.util.stream.Collectors;
2222

23+
import org.springframework.beans.factory.config.BeanDefinition;
24+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2325
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
26+
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
2427
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
28+
import org.springframework.core.Conventions;
2529
import org.springframework.core.annotation.MergedAnnotation;
2630
import org.springframework.core.type.AnnotationMetadata;
2731

@@ -30,12 +34,17 @@
3034
* {@link EnableConfigurationProperties @EnableConfigurationProperties}.
3135
*
3236
* @author Phillip Webb
37+
* @author Andy Wilkinson
3338
*/
3439
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
3540

41+
private static final String METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME = Conventions
42+
.getQualifiedAttributeName(EnableConfigurationPropertiesRegistrar.class, "methodValidationExcludeFilter");
43+
3644
@Override
3745
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
3846
registerInfrastructureBeans(registry);
47+
registerMethodValidationExcludeFilter(registry);
3948
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
4049
getTypes(metadata).forEach(beanRegistrar::register);
4150
}
@@ -51,4 +60,14 @@ static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
5160
BoundConfigurationProperties.register(registry);
5261
}
5362

63+
static void registerMethodValidationExcludeFilter(BeanDefinitionRegistry registry) {
64+
if (!registry.containsBeanDefinition(METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME)) {
65+
BeanDefinition definition = BeanDefinitionBuilder
66+
.genericBeanDefinition(MethodValidationExcludeFilter.class,
67+
() -> MethodValidationExcludeFilter.byAnnotation(ConfigurationProperties.class))
68+
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition();
69+
registry.registerBeanDefinition(METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME, definition);
70+
}
71+
}
72+
5473
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.validation.beanvalidation;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
23+
24+
import org.springframework.aop.ClassFilter;
25+
import org.springframework.aop.MethodMatcher;
26+
import org.springframework.aop.support.ComposablePointcut;
27+
import org.springframework.aop.support.DefaultPointcutAdvisor;
28+
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
29+
30+
/**
31+
* Custom {@link MethodValidationPostProcessor} that applies
32+
* {@code MethodValidationExclusionFilter exclusion filters}.
33+
*
34+
* @author Andy Wilkinson
35+
* @since 2.4.0
36+
*/
37+
public class FilteredMethodValidationPostProcessor extends MethodValidationPostProcessor {
38+
39+
private final Collection<MethodValidationExcludeFilter> excludeFilters;
40+
41+
/**
42+
* Creates a new {@code ExcludingMethodValidationPostProcessor} that will apply the
43+
* given {@code exclusionFilters} when identifying beans that are eligible for method
44+
* validation post-processing.
45+
* @param excludeFilters filters to apply
46+
*/
47+
public FilteredMethodValidationPostProcessor(Stream<? extends MethodValidationExcludeFilter> excludeFilters) {
48+
this.excludeFilters = excludeFilters.collect(Collectors.toList());
49+
}
50+
51+
/**
52+
* Creates a new {@code ExcludingMethodValidationPostProcessor} that will apply the
53+
* given {@code exclusionFilters} when identifying beans that are eligible for method
54+
* validation post-processing.
55+
* @param excludeFilters filters to apply
56+
*/
57+
public FilteredMethodValidationPostProcessor(Collection<? extends MethodValidationExcludeFilter> excludeFilters) {
58+
this.excludeFilters = new ArrayList<>(excludeFilters);
59+
}
60+
61+
@Override
62+
public void afterPropertiesSet() {
63+
super.afterPropertiesSet();
64+
DefaultPointcutAdvisor advisor = (DefaultPointcutAdvisor) this.advisor;
65+
ClassFilter classFilter = advisor.getPointcut().getClassFilter();
66+
MethodMatcher methodMatcher = advisor.getPointcut().getMethodMatcher();
67+
advisor.setPointcut(new ComposablePointcut(classFilter, methodMatcher).intersection(this::isIncluded));
68+
}
69+
70+
private boolean isIncluded(Class<?> candidate) {
71+
for (MethodValidationExcludeFilter exclusionFilter : this.excludeFilters) {
72+
if (exclusionFilter.isExcluded(candidate)) {
73+
return false;
74+
}
75+
}
76+
return true;
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.validation.beanvalidation;
18+
19+
import java.lang.annotation.Annotation;
20+
21+
import org.springframework.core.annotation.MergedAnnotations;
22+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
23+
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
24+
25+
/**
26+
* A filter for excluding types from method validation.
27+
*
28+
* @author Andy Wilkinson
29+
* @since 2.4.0
30+
* @see MethodValidationPostProcessor
31+
*/
32+
public interface MethodValidationExcludeFilter {
33+
34+
/**
35+
* Evaluate whether to exclude the given {@code type} from method validation.
36+
* @param type the type to evaluate
37+
* @return {@code true} to exclude the type from method validation, otherwise
38+
* {@code false}.
39+
*/
40+
boolean isExcluded(Class<?> type);
41+
42+
/**
43+
* Factory method to crate a {@link MethodValidationExcludeFilter} that excludes
44+
* classes by annotation.
45+
* @param annotationType the annotation to check
46+
* @return a {@link MethodValidationExcludeFilter} instance
47+
*/
48+
static MethodValidationExcludeFilter byAnnotation(Class<? extends Annotation> annotationType) {
49+
return byAnnotation(annotationType, SearchStrategy.INHERITED_ANNOTATIONS);
50+
}
51+
52+
/**
53+
* Factory method to crate a {@link MethodValidationExcludeFilter} that excludes
54+
* classes by annotation.
55+
* @param annotationType the annotation to check
56+
* @param searchStrategy the annotation search strategy
57+
* @return a {@link MethodValidationExcludeFilter} instance
58+
*/
59+
static MethodValidationExcludeFilter byAnnotation(Class<? extends Annotation> annotationType,
60+
SearchStrategy searchStrategy) {
61+
return (type) -> MergedAnnotations.from(type, SearchStrategy.SUPERCLASS).isPresent(annotationType);
62+
}
63+
64+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Utilities and classes related to bean validation.
19+
*/
20+
package org.springframework.boot.validation.beanvalidation;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.validation.beanvalidation;
18+
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
/**
27+
* Tests for {@link MethodValidationExcludeFilter}.
28+
*
29+
* @author Andy Wilkinson
30+
*/
31+
class MethodValidationExcludeFilterTests {
32+
33+
@Test
34+
void byAnnotationWhenClassIsAnnotatedExcludes() {
35+
MethodValidationExcludeFilter filter = MethodValidationExcludeFilter.byAnnotation(Indicator.class);
36+
assertThat(filter.isExcluded(Annotated.class)).isTrue();
37+
}
38+
39+
@Test
40+
void byAnnotationWhenClassIsNotAnnotatedIncludes() {
41+
MethodValidationExcludeFilter filter = MethodValidationExcludeFilter.byAnnotation(Indicator.class);
42+
assertThat(filter.isExcluded(Plain.class)).isFalse();
43+
}
44+
45+
static class Plain {
46+
47+
}
48+
49+
@Indicator
50+
static class Annotated {
51+
52+
}
53+
54+
@Retention(RetentionPolicy.RUNTIME)
55+
@interface Indicator {
56+
57+
}
58+
59+
}

spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleConfigurationProperties.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@
2323

2424
@Validated
2525
@ConfigurationProperties(prefix = "sample")
26-
public class SampleConfigurationProperties {
26+
public final class SampleConfigurationProperties {
2727

2828
@NotNull
2929
private String name;

0 commit comments

Comments
 (0)