diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java new file mode 100644 index 0000000000..ad32b2c998 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.autoconfigure.validation; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +/** + * Enable the {@code Primary} flag on the auto-configured validator if necessary. + *

+ * As {@link LocalValidatorFactoryBean} exposes 3 validator related contracts and we're + * only checking for the absence {@link javax.validation.Validator}, we should flag the + * auto-configured validator as primary only if no Spring's {@link Validator} is flagged + * as primary. + * + * @author Stephane Nicoll + */ +class PrimaryDefaultValidatorPostProcessor + implements ImportBeanDefinitionRegistrar, BeanFactoryAware { + + /** + * The bean name of the auto-configured Validator. + */ + private static final String VALIDATOR_BEAN_NAME = "defaultValidator"; + + private ConfigurableListableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableListableBeanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + if (this.beanFactory == null) { + return; + } + if (!registry.containsBeanDefinition(VALIDATOR_BEAN_NAME)) { + return; + } + BeanDefinition def = registry.getBeanDefinition(VALIDATOR_BEAN_NAME); + if (def != null + && this.beanFactory.isTypeMatch(VALIDATOR_BEAN_NAME, LocalValidatorFactoryBean.class) + && def.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) { + def.setPrimary(!hasPrimarySpringValidator(registry)); + } + } + + private boolean hasPrimarySpringValidator(BeanDefinitionRegistry registry) { + String[] validatorBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Validator.class, false, false); + for (String validatorBean : validatorBeans) { + BeanDefinition def = registry.getBeanDefinition(validatorBean); + if (def != null && def.isPrimary()) { + return true; + } + } + return false; + } + +} + diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java index 9feba19dad..72be9fd7b4 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.validation.MessageInterpolatorFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Role; import org.springframework.core.env.Environment; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @@ -43,12 +44,13 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess @Configuration @ConditionalOnClass(ExecutableValidator.class) @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") +@Import(PrimaryDefaultValidatorPostProcessor.class) public class ValidationAutoConfiguration { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - @ConditionalOnMissingBean - public static Validator jsr303Validator() { + @ConditionalOnMissingBean(Validator.class) + public static LocalValidatorFactoryBean defaultValidator() { LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java index 1ea207316c..1f83b02e4d 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java @@ -31,15 +31,20 @@ import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link ValidationAutoConfiguration}. * * @author Stephane Nicoll + * @author Phillip Webb */ public class ValidationAutoConfigurationTests { @@ -55,6 +60,95 @@ public class ValidationAutoConfigurationTests { } } + @Test + public void validationAutoConfigurationShouldConfigureDefaultValidator() { + load(Config.class); + String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); + String[] springValidatorNames = this.context + .getBeanNamesForType(org.springframework.validation.Validator.class); + assertThat(jsrValidatorNames).containsExactly("defaultValidator"); + assertThat(springValidatorNames).containsExactly("defaultValidator"); + Validator jsrValidator = this.context.getBean(Validator.class); + org.springframework.validation.Validator springValidator = this.context + .getBean(org.springframework.validation.Validator.class); + assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); + assertThat(jsrValidator).isEqualTo(springValidator); + assertThat(isPrimaryBean("defaultValidator")).isTrue(); + } + + @Test + public void validationAutoConfigurationWhenUserProvidesValidatorShouldBackOff() { + load(UserDefinedValidatorConfig.class); + String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); + String[] springValidatorNames = this.context + .getBeanNamesForType(org.springframework.validation.Validator.class); + assertThat(jsrValidatorNames).containsExactly("customValidator"); + assertThat(springValidatorNames).containsExactly("customValidator"); + org.springframework.validation.Validator springValidator = this.context + .getBean(org.springframework.validation.Validator.class); + Validator jsrValidator = this.context.getBean(Validator.class); + assertThat(jsrValidator).isInstanceOf(OptionalValidatorFactoryBean.class); + assertThat(jsrValidator).isEqualTo(springValidator); + assertThat(isPrimaryBean("customValidator")).isFalse(); + } + + @Test + public void validationAutoConfigurationWhenUserProvidesDefaultValidatorShouldNotEnablePrimary() { + load(UserDefinedDefaultValidatorConfig.class); + String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); + String[] springValidatorNames = this.context + .getBeanNamesForType(org.springframework.validation.Validator.class); + assertThat(jsrValidatorNames).containsExactly("defaultValidator"); + assertThat(springValidatorNames).containsExactly("defaultValidator"); + assertThat(isPrimaryBean("defaultValidator")).isFalse(); + } + + @Test + public void validationAutoConfigurationWhenUserProvidesJsrValidatorShouldBackOff() { + load(UserDefinedJsrValidatorConfig.class); + String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); + String[] springValidatorNames = this.context + .getBeanNamesForType(org.springframework.validation.Validator.class); + assertThat(jsrValidatorNames).containsExactly("customValidator"); + assertThat(springValidatorNames).isEmpty(); + assertThat(isPrimaryBean("customValidator")).isFalse(); + } + + @Test + public void validationAutoConfigurationWhenUserProvidesSpringValidatorShouldCreateJsrValidator() { + load(UserDefinedSpringValidatorConfig.class); + String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); + String[] springValidatorNames = this.context + .getBeanNamesForType(org.springframework.validation.Validator.class); + assertThat(jsrValidatorNames).containsExactly("defaultValidator"); + assertThat(springValidatorNames).containsExactly( + "customValidator", "anotherCustomValidator", "defaultValidator"); + Validator jsrValidator = this.context.getBean(Validator.class); + org.springframework.validation.Validator springValidator = this.context + .getBean(org.springframework.validation.Validator.class); + assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); + assertThat(jsrValidator).isEqualTo(springValidator); + assertThat(isPrimaryBean("defaultValidator")).isTrue(); + } + + @Test + public void validationAutoConfigurationWhenUserProvidesPrimarySpringValidatorShouldRemovePrimaryFlag() { + load(UserDefinedPrimarySpringValidatorConfig.class); + String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); + String[] springValidatorNames = this.context + .getBeanNamesForType(org.springframework.validation.Validator.class); + assertThat(jsrValidatorNames).containsExactly("defaultValidator"); + assertThat(springValidatorNames).containsExactly( + "customValidator", "anotherCustomValidator", "defaultValidator"); + Validator jsrValidator = this.context.getBean(Validator.class); + org.springframework.validation.Validator springValidator = this.context + .getBean(org.springframework.validation.Validator.class); + assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); + assertThat(springValidator).isEqualTo( + this.context.getBean("anotherCustomValidator")); + assertThat(isPrimaryBean("defaultValidator")).isFalse(); + } + @Test public void validationIsEnabled() { load(SampleService.class); @@ -104,7 +198,11 @@ public class ValidationAutoConfigurationTests { .getPropertyValue("validator")); } - public void load(Class config, String... environment) { + private boolean isPrimaryBean(String beanName) { + return this.context.getBeanDefinition(beanName).isPrimary(); + } + + private void load(Class config, String... environment) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); EnvironmentTestUtils.addEnvironment(ctx, environment); if (config != null) { @@ -115,6 +213,72 @@ public class ValidationAutoConfigurationTests { this.context = ctx; } + @Configuration + static class Config { + + } + + @Configuration + static class UserDefinedValidatorConfig { + + @Bean + public OptionalValidatorFactoryBean customValidator() { + return new OptionalValidatorFactoryBean(); + } + + } + + @Configuration + static class UserDefinedDefaultValidatorConfig { + + @Bean + public OptionalValidatorFactoryBean defaultValidator() { + return new OptionalValidatorFactoryBean(); + } + + } + + @Configuration + static class UserDefinedJsrValidatorConfig { + + @Bean + public Validator customValidator() { + return mock(Validator.class); + } + + } + + @Configuration + static class UserDefinedSpringValidatorConfig { + + @Bean + public org.springframework.validation.Validator customValidator() { + return mock(org.springframework.validation.Validator.class); + } + + @Bean + public org.springframework.validation.Validator anotherCustomValidator() { + return mock(org.springframework.validation.Validator.class); + } + + } + + @Configuration + static class UserDefinedPrimarySpringValidatorConfig { + + @Bean + public org.springframework.validation.Validator customValidator() { + return mock(org.springframework.validation.Validator.class); + } + + @Bean + @Primary + public org.springframework.validation.Validator anotherCustomValidator() { + return mock(org.springframework.validation.Validator.class); + } + + } + @Validated static class SampleService { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java index 724486d37d..47e2c2ae4f 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfigurationTests.java @@ -39,6 +39,7 @@ import org.junit.rules.ExpectedException; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WelcomePageHandlerMapping; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; @@ -59,6 +60,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.validation.Validator; @@ -655,77 +657,126 @@ public class WebMvcAutoConfigurationTests { } @Test - public void validationNoJsr303ValidatorExposedByDefault() { - load(); + public void validatorWhenNoValidatorShouldUseDefault() { + load(null, new Class[] { ValidationAutoConfiguration.class }); assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) .isEmpty(); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(springValidatorBeans).containsExactly("mvcValidator"); } @Test - public void validationCustomConfigurerTakesPrecedence() { - load(MvcValidator.class); + public void validatorWhenNoCustomizationShouldUseAutoConfigured() { + load(); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(jsrValidatorBeans).containsExactly("defaultValidator"); + assertThat(springValidatorBeans).containsExactly("defaultValidator", "mvcValidator"); + Validator validator = this.context.getBean("mvcValidator", Validator.class); + assertThat(validator).isInstanceOf(WebMvcValidator.class); + Object defaultValidator = this.context.getBean("defaultValidator"); + assertThat(((WebMvcValidator) validator).getTarget()).isSameAs(defaultValidator); + // Primary Spring validator is the one use by MVC behind the scenes + assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator); + } + + @Test + public void validatorWithConfigurerShouldUseSpringValidator() { + load(MvcValidator.class, new Class[] { ValidationAutoConfiguration.class }); assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) .isEmpty(); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - Validator validator = this.context.getBean(Validator.class); - assertThat(validator) + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(springValidatorBeans).containsExactly("mvcValidator"); + assertThat(this.context.getBean("mvcValidator")) .isSameAs(this.context.getBean(MvcValidator.class).validator); } @Test - public void validationCustomConfigurerTakesPrecedenceAndDoNotExposeJsr303() { - load(MvcJsr303Validator.class); + public void validatorWithConfigurerDoesNotExposeJsr303() { + load(MvcJsr303Validator.class, new Class[] { ValidationAutoConfiguration.class }); assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) .isEmpty(); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - Validator validator = this.context.getBean(Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(springValidatorBeans).containsExactly("mvcValidator"); + Validator validator = this.context.getBean("mvcValidator", Validator.class); assertThat(validator).isInstanceOf(WebMvcValidator.class); assertThat(((WebMvcValidator) validator).getTarget()) .isSameAs(this.context.getBean(MvcJsr303Validator.class).validator); } @Test - public void validationJsr303CustomValidatorReusedAsSpringValidator() { - load(CustomValidator.class); + public void validatorWithConfigurerTakesPrecedence() { + load(MvcValidator.class); assertThat(this.context.getBeansOfType(ValidatorFactory.class)).hasSize(1); assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) .hasSize(1); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(2); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(springValidatorBeans).containsExactly("defaultValidator", "mvcValidator"); + assertThat(this.context.getBean("mvcValidator")) + .isSameAs(this.context.getBean(MvcValidator.class).validator); + // Primary Spring validator is the auto-configured one as the MVC one has been + // customized via a WebMvcConfigurer + assertThat(this.context.getBean(Validator.class)) + .isEqualTo(this.context.getBean("defaultValidator")); + } + + @Test + public void validatorWithCustomSpringValidatorIgnored() { + load(CustomSpringValidator.class); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(jsrValidatorBeans).containsExactly("defaultValidator"); + assertThat(springValidatorBeans).containsExactly( + "customSpringValidator", "defaultValidator", "mvcValidator"); Validator validator = this.context.getBean("mvcValidator", Validator.class); assertThat(validator).isInstanceOf(WebMvcValidator.class); + Object defaultValidator = this.context.getBean("defaultValidator"); assertThat(((WebMvcValidator) validator).getTarget()) - .isSameAs(this.context.getBean(javax.validation.Validator.class)); + .isSameAs(defaultValidator); + // Primary Spring validator is the one use by MVC behind the scenes + assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator); } @Test - public void validationJsr303ValidatorExposedAsSpringValidator() { - load(Jsr303Validator.class); + public void validatorWithCustomJsr303ValidatorExposedAsSpringValidator() { + load(CustomJsr303Validator.class); assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); - assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) - .hasSize(1); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(jsrValidatorBeans).containsExactly("customJsr303Validator"); + assertThat(springValidatorBeans).containsExactly("mvcValidator"); Validator validator = this.context.getBean(Validator.class); assertThat(validator).isInstanceOf(WebMvcValidator.class); SpringValidatorAdapter target = ((WebMvcValidator) validator) .getTarget(); assertThat(new DirectFieldAccessor(target).getPropertyValue("targetValidator")) - .isSameAs(this.context.getBean(javax.validation.Validator.class)); + .isSameAs(this.context.getBean("customJsr303Validator")); } private void load(Class config, String... environment) { + load(config, null, environment); + } + + private void load(Class config, Class[] exclude, String... environment) { this.context = new AnnotationConfigEmbeddedWebApplicationContext(); EnvironmentTestUtils.addEnvironment(this.context, environment); List> configClasses = new ArrayList>(); if (config != null) { configClasses.add(config); } - configClasses.addAll(Arrays.asList(Config.class, WebMvcAutoConfiguration.class, + configClasses.addAll(Arrays.asList(Config.class, + ValidationAutoConfiguration.class, WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)); + if (!ObjectUtils.isEmpty(exclude)) { + configClasses.removeAll(Arrays.asList(exclude)); + } this.context.register(configClasses.toArray(new Class[configClasses.size()])); this.context.refresh(); } @@ -919,21 +970,21 @@ public class WebMvcAutoConfigurationTests { } @Configuration - static class Jsr303Validator { + static class CustomJsr303Validator { @Bean - public javax.validation.Validator jsr303Validator() { + public javax.validation.Validator customJsr303Validator() { return mock(javax.validation.Validator.class); } } @Configuration - static class CustomValidator { + static class CustomSpringValidator { @Bean - public Validator customValidator() { - return new LocalValidatorFactoryBean(); + public Validator customSpringValidator() { + return mock(Validator.class); } }