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
pull/28993/head
Andy Wilkinson 4 years ago 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Validator;
import javax.validation.executable.ExecutableValidator; import javax.validation.executable.ExecutableValidator;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
import org.springframework.boot.validation.MessageInterpolatorFactory; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -61,8 +64,9 @@ public class ValidationAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) { @Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
excludeFilters.orderedStream());
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true); boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass); processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigurationTests.CustomValidatorConfiguration.TestBeanPostProcessor; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigurationTests.CustomValidatorConfiguration.TestBeanPostProcessor;
import org.springframework.boot.test.util.TestPropertyValues; 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.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -159,6 +161,15 @@ class ValidationAutoConfigurationTests {
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething("KO")); 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 @Test
void validationUsesCglibProxy() { void validationUsesCglibProxy() {
load(DefaultAnotherSampleService.class); 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 { interface AnotherSampleService {
void doSomething(@Min(42) Integer counter); void doSomething(@Min(42) Integer counter);

@ -20,8 +20,12 @@ import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Conventions;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
@ -30,12 +34,17 @@ import org.springframework.core.type.AnnotationMetadata;
* {@link EnableConfigurationProperties @EnableConfigurationProperties}. * {@link EnableConfigurationProperties @EnableConfigurationProperties}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar { class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
private static final String METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME = Conventions
.getQualifiedAttributeName(EnableConfigurationPropertiesRegistrar.class, "methodValidationExcludeFilter");
@Override @Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerInfrastructureBeans(registry); registerInfrastructureBeans(registry);
registerMethodValidationExcludeFilter(registry);
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry); ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
getTypes(metadata).forEach(beanRegistrar::register); getTypes(metadata).forEach(beanRegistrar::register);
} }
@ -51,4 +60,14 @@ class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegi
BoundConfigurationProperties.register(registry); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 @Validated
@ConfigurationProperties(prefix = "sample") @ConfigurationProperties(prefix = "sample")
public class SampleConfigurationProperties { public final class SampleConfigurationProperties {
@NotNull @NotNull
private String name; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication @SpringBootApplication
@EnableConfigurationProperties(SampleConfigurationProperties.class)
public class SampleSimpleApplication implements CommandLineRunner { public class SampleSimpleApplication implements CommandLineRunner {
// Simple example shows how a command line spring application can execute an // Simple example shows how a command line spring application can execute an

Loading…
Cancel
Save