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
This commit is contained in:
committed by
Phillip Webb
parent
a0862f9146
commit
f60f3cb38e
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2020 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.
|
||||
@@ -19,12 +19,15 @@ package org.springframework.boot.autoconfigure.validation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.executable.ExecutableValidator;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
|
||||
import org.springframework.boot.validation.MessageInterpolatorFactory;
|
||||
import org.springframework.boot.validation.beanvalidation.FilteredMethodValidationPostProcessor;
|
||||
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -61,8 +64,9 @@ public class ValidationAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
|
||||
@Lazy Validator validator) {
|
||||
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
|
||||
@Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
|
||||
FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
|
||||
excludeFilters.orderedStream());
|
||||
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
|
||||
processor.setProxyTargetClass(proxyTargetClass);
|
||||
processor.setValidator(validator);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2020 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.
|
||||
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigurationTests.CustomValidatorConfiguration.TestBeanPostProcessor;
|
||||
import org.springframework.boot.test.util.TestPropertyValues;
|
||||
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -43,6 +44,7 @@ import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBea
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -159,6 +161,15 @@ class ValidationAutoConfigurationTests {
|
||||
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething("KO"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void classCanBeExcludedFromValidation() {
|
||||
load(ExcludedServiceConfiguration.class);
|
||||
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1);
|
||||
ExcludedService service = this.context.getBean(ExcludedService.class);
|
||||
service.doSomething("Valid");
|
||||
assertThatNoException().isThrownBy(() -> service.doSomething("KO"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationUsesCglibProxy() {
|
||||
load(DefaultAnotherSampleService.class);
|
||||
@@ -285,6 +296,29 @@ class ValidationAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static final class ExcludedServiceConfiguration {
|
||||
|
||||
@Bean
|
||||
ExcludedService excludedService() {
|
||||
return new ExcludedService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MethodValidationExcludeFilter exclusionFilter() {
|
||||
return (type) -> type.equals(ExcludedService.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Validated
|
||||
static final class ExcludedService {
|
||||
|
||||
void doSomething(@Size(min = 3, max = 10) String name) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface AnotherSampleService {
|
||||
|
||||
void doSomething(@Min(42) Integer counter);
|
||||
|
||||
@@ -20,8 +20,12 @@ import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
@@ -30,12 +34,17 @@ import org.springframework.core.type.AnnotationMetadata;
|
||||
* {@link EnableConfigurationProperties @EnableConfigurationProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
private static final String METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME = Conventions
|
||||
.getQualifiedAttributeName(EnableConfigurationPropertiesRegistrar.class, "methodValidationExcludeFilter");
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
registerInfrastructureBeans(registry);
|
||||
registerMethodValidationExcludeFilter(registry);
|
||||
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
|
||||
getTypes(metadata).forEach(beanRegistrar::register);
|
||||
}
|
||||
@@ -51,4 +60,14 @@ class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegi
|
||||
BoundConfigurationProperties.register(registry);
|
||||
}
|
||||
|
||||
static void registerMethodValidationExcludeFilter(BeanDefinitionRegistry registry) {
|
||||
if (!registry.containsBeanDefinition(METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME)) {
|
||||
BeanDefinition definition = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(MethodValidationExcludeFilter.class,
|
||||
() -> MethodValidationExcludeFilter.byAnnotation(ConfigurationProperties.class))
|
||||
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE).getBeanDefinition();
|
||||
registry.registerBeanDefinition(METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME, definition);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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.boot.validation.beanvalidation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.aop.ClassFilter;
|
||||
import org.springframework.aop.MethodMatcher;
|
||||
import org.springframework.aop.support.ComposablePointcut;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
|
||||
/**
|
||||
* Custom {@link MethodValidationPostProcessor} that applies
|
||||
* {@code MethodValidationExclusionFilter exclusion filters}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public class FilteredMethodValidationPostProcessor extends MethodValidationPostProcessor {
|
||||
|
||||
private final Collection<MethodValidationExcludeFilter> excludeFilters;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ExcludingMethodValidationPostProcessor} that will apply the
|
||||
* given {@code exclusionFilters} when identifying beans that are eligible for method
|
||||
* validation post-processing.
|
||||
* @param excludeFilters filters to apply
|
||||
*/
|
||||
public FilteredMethodValidationPostProcessor(Stream<? extends MethodValidationExcludeFilter> excludeFilters) {
|
||||
this.excludeFilters = excludeFilters.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ExcludingMethodValidationPostProcessor} that will apply the
|
||||
* given {@code exclusionFilters} when identifying beans that are eligible for method
|
||||
* validation post-processing.
|
||||
* @param excludeFilters filters to apply
|
||||
*/
|
||||
public FilteredMethodValidationPostProcessor(Collection<? extends MethodValidationExcludeFilter> excludeFilters) {
|
||||
this.excludeFilters = new ArrayList<>(excludeFilters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
DefaultPointcutAdvisor advisor = (DefaultPointcutAdvisor) this.advisor;
|
||||
ClassFilter classFilter = advisor.getPointcut().getClassFilter();
|
||||
MethodMatcher methodMatcher = advisor.getPointcut().getMethodMatcher();
|
||||
advisor.setPointcut(new ComposablePointcut(classFilter, methodMatcher).intersection(this::isIncluded));
|
||||
}
|
||||
|
||||
private boolean isIncluded(Class<?> candidate) {
|
||||
for (MethodValidationExcludeFilter exclusionFilter : this.excludeFilters) {
|
||||
if (exclusionFilter.isExcluded(candidate)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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.boot.validation.beanvalidation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
|
||||
/**
|
||||
* A filter for excluding types from method validation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.4.0
|
||||
* @see MethodValidationPostProcessor
|
||||
*/
|
||||
public interface MethodValidationExcludeFilter {
|
||||
|
||||
/**
|
||||
* Evaluate whether to exclude the given {@code type} from method validation.
|
||||
* @param type the type to evaluate
|
||||
* @return {@code true} to exclude the type from method validation, otherwise
|
||||
* {@code false}.
|
||||
*/
|
||||
boolean isExcluded(Class<?> type);
|
||||
|
||||
/**
|
||||
* Factory method to crate a {@link MethodValidationExcludeFilter} that excludes
|
||||
* classes by annotation.
|
||||
* @param annotationType the annotation to check
|
||||
* @return a {@link MethodValidationExcludeFilter} instance
|
||||
*/
|
||||
static MethodValidationExcludeFilter byAnnotation(Class<? extends Annotation> annotationType) {
|
||||
return byAnnotation(annotationType, SearchStrategy.INHERITED_ANNOTATIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to crate a {@link MethodValidationExcludeFilter} that excludes
|
||||
* classes by annotation.
|
||||
* @param annotationType the annotation to check
|
||||
* @param searchStrategy the annotation search strategy
|
||||
* @return a {@link MethodValidationExcludeFilter} instance
|
||||
*/
|
||||
static MethodValidationExcludeFilter byAnnotation(Class<? extends Annotation> annotationType,
|
||||
SearchStrategy searchStrategy) {
|
||||
return (type) -> MergedAnnotations.from(type, SearchStrategy.SUPERCLASS).isPresent(annotationType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utilities and classes related to bean validation.
|
||||
*/
|
||||
package org.springframework.boot.validation.beanvalidation;
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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.boot.validation.beanvalidation;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MethodValidationExcludeFilter}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class MethodValidationExcludeFilterTests {
|
||||
|
||||
@Test
|
||||
void byAnnotationWhenClassIsAnnotatedExcludes() {
|
||||
MethodValidationExcludeFilter filter = MethodValidationExcludeFilter.byAnnotation(Indicator.class);
|
||||
assertThat(filter.isExcluded(Annotated.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void byAnnotationWhenClassIsNotAnnotatedIncludes() {
|
||||
MethodValidationExcludeFilter filter = MethodValidationExcludeFilter.byAnnotation(Indicator.class);
|
||||
assertThat(filter.isExcluded(Plain.class)).isFalse();
|
||||
}
|
||||
|
||||
static class Plain {
|
||||
|
||||
}
|
||||
|
||||
@Indicator
|
||||
static class Annotated {
|
||||
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Indicator {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2020 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.
|
||||
@@ -23,7 +23,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "sample")
|
||||
public class SampleConfigurationProperties {
|
||||
public final class SampleConfigurationProperties {
|
||||
|
||||
@NotNull
|
||||
private String name;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* Copyright 2012-2020 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.
|
||||
@@ -22,8 +22,10 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties(SampleConfigurationProperties.class)
|
||||
public class SampleSimpleApplication implements CommandLineRunner {
|
||||
|
||||
// Simple example shows how a command line spring application can execute an
|
||||
|
||||
Reference in New Issue
Block a user