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