diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java index 57d48be918..9c01dfc252 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java @@ -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 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); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java index fcc05056b4..83dbedd585 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java index 8ce026b280..49aa20d1aa 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java @@ -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); + } + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/FilteredMethodValidationPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/FilteredMethodValidationPostProcessor.java new file mode 100644 index 0000000000..0b941fc3c1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/FilteredMethodValidationPostProcessor.java @@ -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 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 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 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; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/MethodValidationExcludeFilter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/MethodValidationExcludeFilter.java new file mode 100644 index 0000000000..546dc63a2b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/MethodValidationExcludeFilter.java @@ -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 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 annotationType, + SearchStrategy searchStrategy) { + return (type) -> MergedAnnotations.from(type, SearchStrategy.SUPERCLASS).isPresent(annotationType); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/package-info.java new file mode 100644 index 0000000000..a50b06728d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/beanvalidation/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/validation/beanvalidation/MethodValidationExcludeFilterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/validation/beanvalidation/MethodValidationExcludeFilterTests.java new file mode 100644 index 0000000000..3976d5bbbd --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/validation/beanvalidation/MethodValidationExcludeFilterTests.java @@ -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 { + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleConfigurationProperties.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleConfigurationProperties.java index 9c9465f07f..449e6aea59 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleConfigurationProperties.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleConfigurationProperties.java @@ -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; diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleSimpleApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleSimpleApplication.java index 2c59d0a836..2f9f0cb275 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleSimpleApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/java/smoketest/simple/SampleSimpleApplication.java @@ -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