From f42998f5ca958514af99a12f18ae395c88541018 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 25 Apr 2017 17:14:59 +0200 Subject: [PATCH 1/2] Revert "Refine validator and MVC validator configuration" This commit reverts c9561f0 and 69a8c0d and effectivly fixes gh-8979 but reintroduces the issue reported in gh-8495. --- .../EndpointMvcIntegrationTests.java | 7 +- .../DefaultValidatorConfiguration.java | 47 ---- .../validation/DelegatingValidator.java | 82 ------ .../Jsr303ValidatorAdapterConfiguration.java | 46 ---- .../ValidationAutoConfiguration.java | 19 +- .../web/WebMvcAutoConfiguration.java | 173 +----------- .../autoconfigure/web/WebMvcValidator.java | 144 ++++++++++ ...ringBootWebSecurityConfigurationTests.java | 7 +- .../validation/DelegatingValidatorTests.java | 116 --------- .../ValidationAutoConfigurationTests.java | 152 +++-------- ...asicErrorControllerDirectMockMvcTests.java | 7 +- .../web/BasicErrorControllerMockMvcTests.java | 7 +- .../web/WebMvcAutoConfigurationTests.java | 246 +++++------------- .../web/WebMvcValidatorTests.java | 152 +++++++++++ 14 files changed, 433 insertions(+), 772 deletions(-) delete mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DefaultValidatorConfiguration.java delete mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java delete mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/Jsr303ValidatorAdapterConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcValidator.java delete mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcValidatorTests.java diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java index 146b99e4b4..d6312f7dc3 100755 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java @@ -41,7 +41,6 @@ import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCusto import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; @@ -157,9 +156,9 @@ public class EndpointMvcIntegrationTests { @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class, - WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, - ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + JacksonAutoConfiguration.class, ErrorMvcAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) protected @interface MinimalWebConfiguration { } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DefaultValidatorConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DefaultValidatorConfiguration.java deleted file mode 100644 index 9311a272c6..0000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DefaultValidatorConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.factory.config.BeanDefinition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.validation.MessageInterpolatorFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; - -/** - * Default validator configuration imported by {@link ValidationAutoConfiguration}. - * - * @author Stephane Nicoll - * @author Phillip Webb - */ -@Configuration -class DefaultValidatorConfiguration { - - @Bean - @ConditionalOnMissingBean(type = { "javax.validation.Validator", - "org.springframework.validation.Validator" }) - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public static LocalValidatorFactoryBean defaultValidator() { - LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); - MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); - factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); - return factoryBean; - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java deleted file mode 100644 index e180ba284a..0000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.util.Assert; -import org.springframework.validation.Errors; -import org.springframework.validation.SmartValidator; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.SpringValidatorAdapter; - -/** - * {@link Validator} implementation that delegates calls to another {@link Validator}. - * This {@link Validator} implements Spring's {@link SmartValidator} interface but does - * not implement the JSR-303 {@code javax.validator.Validator} interface. - * - * @author Phillip Webb - * @since 1.5.3 - */ -public class DelegatingValidator implements SmartValidator { - - private final Validator delegate; - - /** - * Create a new {@link DelegatingValidator} instance. - * @param targetValidator the target JSR validator - */ - public DelegatingValidator(javax.validation.Validator targetValidator) { - this.delegate = new SpringValidatorAdapter(targetValidator); - } - - /** - * Create a new {@link DelegatingValidator} instance. - * @param targetValidator the target validator - */ - public DelegatingValidator(Validator targetValidator) { - Assert.notNull(targetValidator, "Target Validator must not be null"); - this.delegate = targetValidator; - } - - @Override - public boolean supports(Class clazz) { - return this.delegate.supports(clazz); - } - - @Override - public void validate(Object target, Errors errors) { - this.delegate.validate(target, errors); - } - - @Override - public void validate(Object target, Errors errors, Object... validationHints) { - if (this.delegate instanceof SmartValidator) { - ((SmartValidator) this.delegate).validate(target, errors, validationHints); - } - else { - this.delegate.validate(target, errors); - } - } - - /** - * Return the delegate validator. - * @return the delegate validator - */ - protected final Validator getDelegate() { - return this.delegate; - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/Jsr303ValidatorAdapterConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/Jsr303ValidatorAdapterConfiguration.java deleted file mode 100644 index 2573dd3e3c..0000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/Jsr303ValidatorAdapterConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 javax.validation.Validator; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.validation.SmartValidator; - -/** - * JSR 303 adapter configration imported by {@link ValidationAutoConfiguration}. - * - * @author Stephane Nicoll - * @author Phillip Webb - */ -@Configuration -class Jsr303ValidatorAdapterConfiguration { - - @Bean - @ConditionalOnSingleCandidate(Validator.class) - @ConditionalOnMissingBean(org.springframework.validation.Validator.class) - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public SmartValidator jsr303ValidatorAdapter(Validator validator) { - return new DelegatingValidator(validator); - } - -} 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 99c4efa6b9..9feba19dad 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 @@ -19,16 +19,18 @@ package org.springframework.boot.autoconfigure.validation; import javax.validation.Validator; import javax.validation.executable.ExecutableValidator; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.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; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; /** @@ -41,12 +43,19 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess @Configuration @ConditionalOnClass(ExecutableValidator.class) @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") -@Import({ DefaultValidatorConfiguration.class, - Jsr303ValidatorAdapterConfiguration.class }) public class ValidationAutoConfiguration { @Bean - @ConditionalOnBean(Validator.class) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnMissingBean + public static Validator jsr303Validator() { + LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); + MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); + factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); + return factoryBean; + } + + @Bean @ConditionalOnMissingBean public static MethodValidationPostProcessor methodValidationPostProcessor( Environment environment, Validator validator) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java index c7f18be85f..1428f80d31 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java @@ -30,18 +30,11 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -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.DisposableBean; -import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -50,38 +43,27 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.validation.DelegatingValidator; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter; import org.springframework.boot.web.filter.OrderedRequestContextFilter; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ConfigurationCondition; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.Resource; -import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.core.type.AnnotationMetadata; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.format.datetime.DateFormatter; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; @@ -169,7 +151,7 @@ public class WebMvcAutoConfiguration { // Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when not // on the classpath @Configuration - @Import({ EnableWebMvcConfiguration.class, MvcValidatorRegistrar.class }) + @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { @@ -378,22 +360,21 @@ public class WebMvcAutoConfiguration { * Configuration equivalent to {@code @EnableWebMvc}. */ @Configuration - public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration - implements InitializingBean { + public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { private final WebMvcProperties mvcProperties; - private final ApplicationContext context; + private final ListableBeanFactory beanFactory; private final WebMvcRegistrations mvcRegistrations; public EnableWebMvcConfiguration( ObjectProvider mvcPropertiesProvider, ObjectProvider mvcRegistrationsProvider, - ApplicationContext context) { + ListableBeanFactory beanFactory) { this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); - this.context = context; + this.beanFactory = beanFactory; } @Bean @@ -424,9 +405,12 @@ public class WebMvcAutoConfiguration { @Bean @Override - @Conditional(DisableMvcValidatorCondition.class) public Validator mvcValidator() { - return this.context.getBean("mvcValidator", Validator.class); + if (!ClassUtils.isPresent("javax.validation.Validator", + getClass().getClassLoader())) { + return super.mvcValidator(); + } + return WebMvcValidator.get(getApplicationContext(), getValidator()); } @Override @@ -441,7 +425,7 @@ public class WebMvcAutoConfiguration { @Override protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() { try { - return this.context.getBean(ConfigurableWebBindingInitializer.class); + return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class); } catch (NoSuchBeanDefinitionException ex) { return super.getConfigurableWebBindingInitializer(); @@ -490,15 +474,6 @@ public class WebMvcAutoConfiguration { return manager; } - @Override - public void afterPropertiesSet() throws Exception { - Assert.state(getValidator() == null, - "Found unexpected validator configuration. A Spring Boot MVC " - + "validator should be registered as bean named " - + "'mvcValidator' and not returned from " - + "WebMvcConfigurer.getValidator()"); - } - } @Configuration @@ -626,130 +601,4 @@ public class WebMvcAutoConfiguration { } - /** - * Condition used to disable the default MVC validator registration. The - * {@link MvcValidatorRegistrar} is actually used to register the {@code mvcValidator} - * bean. - */ - static class DisableMvcValidatorCondition implements ConfigurationCondition { - - @Override - public ConfigurationPhase getConfigurationPhase() { - return ConfigurationPhase.REGISTER_BEAN; - } - - @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - return false; - } - - } - - /** - * {@link ImportBeanDefinitionRegistrar} to deal with the MVC validator bean - * registration. Applies the following rules: - *
    - *
  • With no validators - Uses standard - * {@link WebMvcConfigurationSupport#mvcValidator()} logic.
  • - *
  • With a single validator - Uses an alias.
  • - *
  • With multiple validators - Registers a mvcValidator bean if not already - * defined.
  • - *
- */ - static class MvcValidatorRegistrar - implements ImportBeanDefinitionRegistrar, BeanFactoryAware { - - private static final String JSR303_VALIDATOR_CLASS = "javax.validation.Validator"; - - private BeanFactory beanFactory; - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, - BeanDefinitionRegistry registry) { - if (this.beanFactory instanceof ListableBeanFactory) { - registerOrAliasMvcValidator(registry, - (ListableBeanFactory) this.beanFactory); - } - } - - private void registerOrAliasMvcValidator(BeanDefinitionRegistry registry, - ListableBeanFactory beanFactory) { - String[] validatorBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( - beanFactory, Validator.class, false, false); - if (validatorBeans.length == 0) { - registerNewMvcValidator(registry, beanFactory); - } - else if (validatorBeans.length == 1) { - registry.registerAlias(validatorBeans[0], "mvcValidator"); - } - else { - if (!ObjectUtils.containsElement(validatorBeans, "mvcValidator")) { - registerNewMvcValidator(registry, beanFactory); - } - } - } - - private void registerNewMvcValidator(BeanDefinitionRegistry registry, - ListableBeanFactory beanFactory) { - RootBeanDefinition definition = new RootBeanDefinition(); - definition.setBeanClass(getClass()); - definition.setFactoryMethodName("mvcValidator"); - registry.registerBeanDefinition("mvcValidator", definition); - } - - static Validator mvcValidator() { - Validator validator = new WebMvcConfigurationSupport().mvcValidator(); - try { - if (ClassUtils.forName(JSR303_VALIDATOR_CLASS, null) - .isInstance(validator)) { - return new DelegatingWebMvcValidator(validator); - } - } - catch (Exception ex) { - } - return validator; - } - - } - - /** - * {@link DelegatingValidator} for the MVC validator. - */ - static class DelegatingWebMvcValidator extends DelegatingValidator - implements ApplicationContextAware, InitializingBean, DisposableBean { - - DelegatingWebMvcValidator(Validator targetValidator) { - super(targetValidator); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - if (getDelegate() instanceof ApplicationContextAware) { - ((ApplicationContextAware) getDelegate()) - .setApplicationContext(applicationContext); - } - } - - @Override - public void afterPropertiesSet() throws Exception { - if (getDelegate() instanceof InitializingBean) { - ((InitializingBean) getDelegate()).afterPropertiesSet(); - } - } - - @Override - public void destroy() throws Exception { - if (getDelegate() instanceof DisposableBean) { - ((DisposableBean) getDelegate()).destroy(); - } - } - - } - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcValidator.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcValidator.java new file mode 100644 index 0000000000..499e08dbfd --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcValidator.java @@ -0,0 +1,144 @@ +/* + * 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.web; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.validation.MessageInterpolatorFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.validation.Errors; +import org.springframework.validation.SmartValidator; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.SpringValidatorAdapter; + +/** + * A {@link SmartValidator} exposed as a bean for WebMvc use. Wraps existing + * {@link SpringValidatorAdapter} instances so that only the Spring's {@link Validator} + * type is exposed. This prevents such a bean to expose both the Spring and JSR-303 + * validator contract at the same time. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +class WebMvcValidator implements SmartValidator, ApplicationContextAware, + InitializingBean, DisposableBean { + + private final SpringValidatorAdapter target; + + private final boolean existingBean; + + WebMvcValidator(SpringValidatorAdapter target, boolean existingBean) { + this.target = target; + this.existingBean = existingBean; + } + + SpringValidatorAdapter getTarget() { + return this.target; + } + + @Override + public boolean supports(Class clazz) { + return this.target.supports(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + this.target.validate(target, errors); + } + + @Override + public void validate(Object target, Errors errors, Object... validationHints) { + this.target.validate(target, errors, validationHints); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + if (!this.existingBean && this.target instanceof ApplicationContextAware) { + ((ApplicationContextAware) this.target) + .setApplicationContext(applicationContext); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!this.existingBean && this.target instanceof InitializingBean) { + ((InitializingBean) this.target).afterPropertiesSet(); + } + } + + @Override + public void destroy() throws Exception { + if (!this.existingBean && this.target instanceof DisposableBean) { + ((DisposableBean) this.target).destroy(); + } + } + + public static Validator get(ApplicationContext applicationContext, + Validator validator) { + if (validator != null) { + return wrap(validator, false); + } + return getExistingOrCreate(applicationContext); + } + + private static Validator getExistingOrCreate(ApplicationContext applicationContext) { + Validator existing = getExisting(applicationContext); + if (existing != null) { + return wrap(existing, true); + } + return create(); + } + + private static Validator getExisting(ApplicationContext applicationContext) { + try { + javax.validation.Validator validator = applicationContext + .getBean(javax.validation.Validator.class); + if (validator instanceof Validator) { + return (Validator) validator; + } + return new SpringValidatorAdapter(validator); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + + private static Validator create() { + OptionalValidatorFactoryBean validator = new OptionalValidatorFactoryBean(); + validator.setMessageInterpolator(new MessageInterpolatorFactory().getObject()); + return wrap(validator, false); + } + + private static Validator wrap(Validator validator, boolean existingBean) { + if (validator instanceof javax.validation.Validator) { + if (validator instanceof SpringValidatorAdapter) { + return new WebMvcValidator((SpringValidatorAdapter) validator, + existingBean); + } + return new WebMvcValidator( + new SpringValidatorAdapter((javax.validation.Validator) validator), + existingBean); + } + return validator; + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java index b8d3fddfa1..07812e2891 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java @@ -31,7 +31,6 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; @@ -325,9 +324,9 @@ public class SpringBootWebSecurityConfigurationTests { @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class, - WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) protected @interface MinimalWebConfiguration { } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java deleted file mode 100644 index 04cc4e8c07..0000000000 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.Errors; -import org.springframework.validation.SmartValidator; -import org.springframework.validation.Validator; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link DelegatingValidator}. - * - * @author Phillip Webb - */ -public class DelegatingValidatorTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Mock - private SmartValidator delegate; - - private DelegatingValidator delegating; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - this.delegating = new DelegatingValidator(this.delegate); - } - - @Test - public void createWhenJsrValidatorIsNullShouldThrowException() throws Exception { - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("Target Validator must not be null"); - new DelegatingValidator((javax.validation.Validator) null); - } - - @Test - public void createWithJsrValidatorShouldAdapt() throws Exception { - javax.validation.Validator delegate = mock(javax.validation.Validator.class); - Validator delegating = new DelegatingValidator(delegate); - Object target = new Object(); - Errors errors = new BeanPropertyBindingResult(target, "foo"); - delegating.validate(target, errors); - verify(delegate).validate(any()); - } - - @Test - public void createWithSpringValidatorWhenValidatorIsNullShouldThrowException() - throws Exception { - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("Target Validator must not be null"); - new DelegatingValidator((Validator) null); - } - - @Test - public void supportsShouldDelegateToValidator() throws Exception { - this.delegating.supports(Object.class); - verify(this.delegate).supports(Object.class); - } - - @Test - public void validateShouldDelegateToValidator() throws Exception { - Object target = new Object(); - Errors errors = new BeanPropertyBindingResult(target, "foo"); - this.delegating.validate(target, errors); - verify(this.delegate).validate(target, errors); - } - - @Test - public void validateWithHintsShouldDelegateToValidator() throws Exception { - Object target = new Object(); - Errors errors = new BeanPropertyBindingResult(target, "foo"); - Object[] hints = { "foo", "bar" }; - this.delegating.validate(target, errors, hints); - verify(this.delegate).validate(target, errors, hints); - } - - @Test - public void validateWithHintsWhenDelegateIsNotSmartShouldDelegateToSimpleValidator() - throws Exception { - Validator delegate = mock(Validator.class); - DelegatingValidator delegating = new DelegatingValidator(delegate); - Object target = new Object(); - Errors errors = new BeanPropertyBindingResult(target, "foo"); - Object[] hints = { "foo", "bar" }; - delegating.validate(target, errors, hints); - verify(delegate).validate(target, errors); - } - -} 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 df2e7e3cd9..1ea207316c 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 @@ -32,12 +32,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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}. @@ -59,94 +56,45 @@ public class ValidationAutoConfigurationTests { } @Test - public void validationAutoConfigurationShouldConfigureJsrAndSpringValidator() - throws Exception { - load(Config.class); - Validator jsrValidator = this.context.getBean(Validator.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); - assertThat(jsrValidator).isEqualTo(springValidator); - assertThat(jsrValidatorNames).containsExactly("defaultValidator"); - assertThat(springValidatorNames).containsExactly("defaultValidator"); - } - - @Test - public void validationAutoConfigurationWhenUserProvidesValidatorShouldBackOff() - throws Exception { - load(UserDefinedValidatorConfig.class); - Validator jsrValidator = this.context.getBean(Validator.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidator).isInstanceOf(OptionalValidatorFactoryBean.class); - assertThat(jsrValidator).isEqualTo(springValidator); - assertThat(jsrValidatorNames).containsExactly("customValidator"); - assertThat(springValidatorNames).containsExactly("customValidator"); - } - - @Test - public void validationAutoConfigurationWhenUserProvidesJsrOnlyShouldAdaptIt() - throws Exception { - load(UserDefinedJsrValidatorConfig.class); - Validator jsrValidator = this.context.getBean(Validator.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidator).isNotEqualTo(springValidator); - assertThat(springValidator).isInstanceOf(DelegatingValidator.class); - assertThat(jsrValidatorNames).containsExactly("customValidator"); - assertThat(springValidatorNames).containsExactly("jsr303ValidatorAdapter"); - } - - @Test - public void validationAutoConfigurationShouldBeEnabled() { - load(ClassWithConstraint.class); + public void validationIsEnabled() { + load(SampleService.class); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - ClassWithConstraint service = this.context.getBean(ClassWithConstraint.class); - service.call("Valid"); + SampleService service = this.context.getBean(SampleService.class); + service.doSomething("Valid"); this.thrown.expect(ConstraintViolationException.class); - service.call("KO"); + service.doSomething("KO"); } @Test - public void validationAutoConfigurationShouldUseCglibProxy() { - load(ImplementationOfInterfaceWithConstraint.class); + public void validationUsesCglibProxy() { + load(DefaultAnotherSampleService.class); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - ImplementationOfInterfaceWithConstraint service = this.context - .getBean(ImplementationOfInterfaceWithConstraint.class); - service.call(42); + DefaultAnotherSampleService service = this.context + .getBean(DefaultAnotherSampleService.class); + service.doSomething(42); this.thrown.expect(ConstraintViolationException.class); - service.call(2); + service.doSomething(2); } @Test - public void validationAutoConfigurationWhenProxyTargetClassIsFalseShouldUseJdkProxy() { + public void validationCanBeConfiguredToUseJdkProxy() { load(AnotherSampleServiceConfiguration.class, "spring.aop.proxy-target-class=false"); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - assertThat(this.context - .getBeansOfType(ImplementationOfInterfaceWithConstraint.class)).isEmpty(); - InterfaceWithConstraint service = this.context - .getBean(InterfaceWithConstraint.class); - service.call(42); + assertThat(this.context.getBeansOfType(DefaultAnotherSampleService.class)) + .isEmpty(); + AnotherSampleService service = this.context.getBean(AnotherSampleService.class); + service.doSomething(42); this.thrown.expect(ConstraintViolationException.class); - service.call(2); + service.doSomething(2); } @Test - public void validationAutoConfigurationWhenUserDefinesMethodValidationPostProcessorShouldBackOff() { - load(UserDefinedMethodValidationConfig.class); + public void userDefinedMethodValidationPostProcessorTakesPrecedence() { + load(SampleConfiguration.class); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); Object userMethodValidationPostProcessor = this.context - .getBean("customMethodValidationPostProcessor"); + .getBean("testMethodValidationPostProcessor"); assertThat(this.context.getBean(MethodValidationPostProcessor.class)) .isSameAs(userMethodValidationPostProcessor); assertThat(this.context.getBeansOfType(MethodValidationPostProcessor.class)) @@ -167,73 +115,47 @@ public class ValidationAutoConfigurationTests { this.context = ctx; } - @Configuration - static class Config { + @Validated + static class SampleService { - } + public void doSomething(@Size(min = 3, max = 10) String name) { - @Configuration - static class UserDefinedValidatorConfig { - - @Bean - public OptionalValidatorFactoryBean customValidator() { - return new OptionalValidatorFactoryBean(); } } - @Configuration - static class UserDefinedJsrValidatorConfig { - - @Bean - public Validator customValidator() { - return mock(Validator.class); - } + interface AnotherSampleService { + void doSomething(@Min(42) Integer counter); } - @Configuration - static class UserDefinedMethodValidationConfig { + @Validated + static class DefaultAnotherSampleService implements AnotherSampleService { + + @Override + public void doSomething(Integer counter) { - @Bean - public MethodValidationPostProcessor customMethodValidationPostProcessor() { - return new MethodValidationPostProcessor(); } - } @Configuration static class AnotherSampleServiceConfiguration { @Bean - public InterfaceWithConstraint implementationOfInterfaceWithConstraint() { - return new ImplementationOfInterfaceWithConstraint(); + public AnotherSampleService anotherSampleService() { + return new DefaultAnotherSampleService(); } } - @Validated - static class ClassWithConstraint { - - public void call(@Size(min = 3, max = 10) String name) { + @Configuration + static class SampleConfiguration { + @Bean + public MethodValidationPostProcessor testMethodValidationPostProcessor() { + return new MethodValidationPostProcessor(); } } - interface InterfaceWithConstraint { - - void call(@Min(42) Integer counter); - } - - @Validated - static class ImplementationOfInterfaceWithConstraint - implements InterfaceWithConstraint { - - @Override - public void call(Integer counter) { - - } - } - } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java index 29bb265dbd..4879481093 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerDirectMockMvcTests.java @@ -35,7 +35,6 @@ import org.junit.rules.ExpectedException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.util.ApplicationContextTestUtils; import org.springframework.context.annotation.Configuration; @@ -127,9 +126,9 @@ public class BasicErrorControllerDirectMockMvcTests { @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class, - WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) protected @interface MinimalWebConfiguration { } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java index da5a0c5820..3ed4e8a9a0 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/BasicErrorControllerMockMvcTests.java @@ -35,7 +35,6 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -129,9 +128,9 @@ public class BasicErrorControllerMockMvcTests { @Import({ EmbeddedServletContainerAutoConfiguration.EmbeddedTomcat.class, EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class, - WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) protected @interface MinimalWebConfiguration { } 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 ca0355ce42..724486d37d 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 @@ -27,6 +27,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.validation.ValidatorFactory; import org.assertj.core.api.Condition; import org.joda.time.DateTime; @@ -36,11 +37,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.validation.DelegatingValidator; -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; @@ -61,11 +59,11 @@ 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; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.SpringValidatorAdapter; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.filter.HttpPutFormContentFilter; @@ -657,154 +655,77 @@ public class WebMvcAutoConfigurationTests { } @Test - public void validatorWhenSuppliedByConfigurerShouldThrowException() throws Exception { - this.thrown.expect(BeanCreationException.class); - this.thrown.expectMessage("unexpected validator configuration"); - load(ValidatorWebMvcConfigurer.class); - } - - @Test - public void validatorWhenAutoConfiguredShouldUseAlias() throws Exception { + public void validationNoJsr303ValidatorExposedByDefault() { load(); - Object defaultValidator = this.context.getBean("defaultValidator"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isSameAs(defaultValidator); - assertThat(springValidatorBeans).containsExactly("defaultValidator"); - assertThat(jsrValidatorBeans).containsExactly("defaultValidator"); + assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); + assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) + .isEmpty(); + assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); } @Test - public void validatorWhenUserDefinedSpringOnlyShouldUseDefined() throws Exception { - load(UserDefinedSpringOnlyValidator.class); - Object customValidator = this.context.getBean("customValidator"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isSameAs(customValidator); - assertThat(this.context.getBean(Validator.class)).isEqualTo(customValidator); - assertThat(springValidatorBeans).containsExactly("customValidator"); - assertThat(jsrValidatorBeans).isEmpty(); + public void validationCustomConfigurerTakesPrecedence() { + load(MvcValidator.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) + .isSameAs(this.context.getBean(MvcValidator.class).validator); } @Test - public void validatorWhenUserDefinedJsr303ShouldAdapt() throws Exception { - load(UserDefinedJsr303Validator.class); - Object customValidator = this.context.getBean("customValidator"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isNotSameAs(customValidator); - assertThat(this.context.getBean(javax.validation.Validator.class)) - .isEqualTo(customValidator); - assertThat(springValidatorBeans).containsExactly("jsr303ValidatorAdapter"); - assertThat(jsrValidatorBeans).containsExactly("customValidator"); + public void validationCustomConfigurerTakesPrecedenceAndDoNotExposeJsr303() { + load(MvcJsr303Validator.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).isInstanceOf(WebMvcValidator.class); + assertThat(((WebMvcValidator) validator).getTarget()) + .isSameAs(this.context.getBean(MvcJsr303Validator.class).validator); } @Test - public void validatorWhenUserDefinedSingleJsr303AndSpringShouldUseDefined() - throws Exception { - load(UserDefinedSingleJsr303AndSpringValidator.class); - Object customValidator = this.context.getBean("customValidator"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isSameAs(customValidator); - assertThat(this.context.getBean(javax.validation.Validator.class)) - .isEqualTo(customValidator); - assertThat(this.context.getBean(Validator.class)).isEqualTo(customValidator); - assertThat(springValidatorBeans).containsExactly("customValidator"); - assertThat(jsrValidatorBeans).containsExactly("customValidator"); + public void validationJsr303CustomValidatorReusedAsSpringValidator() { + load(CustomValidator.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); + Validator validator = this.context.getBean("mvcValidator", Validator.class); + assertThat(validator).isInstanceOf(WebMvcValidator.class); + assertThat(((WebMvcValidator) validator).getTarget()) + .isSameAs(this.context.getBean(javax.validation.Validator.class)); } @Test - public void validatorWhenUserDefinedJsr303AndSpringShouldUseDefined() - throws Exception { - load(UserDefinedJsr303AndSpringValidator.class); - Object customJsrValidator = this.context.getBean("customJsrValidator"); - Object customSpringValidator = this.context.getBean("customSpringValidator"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(customJsrValidator).isNotSameAs(customSpringValidator); - assertThat(mvcValidator).isSameAs(customSpringValidator); - assertThat(this.context.getBean(javax.validation.Validator.class)) - .isEqualTo(customJsrValidator); - assertThat(this.context.getBean(Validator.class)) - .isEqualTo(customSpringValidator); - assertThat(springValidatorBeans).containsExactly("customSpringValidator"); - assertThat(jsrValidatorBeans).containsExactly("customJsrValidator"); - } - - @Test - public void validatorWhenExcludingValidatorAutoConfigurationShouldUseMvc() - throws Exception { - load(null, new Class[] { ValidationAutoConfiguration.class }); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isInstanceOf(DelegatingValidator.class); - assertThat(springValidatorBeans).containsExactly("mvcValidator"); - assertThat(jsrValidatorBeans).isEmpty(); - } - - @Test - public void validatorWhenMultipleValidatorsAndNoMvcValidatorShouldAddMvc() - throws Exception { - load(MultipleValidatorsAndNoMvcValidator.class); - Object customValidator1 = this.context.getBean("customValidator1"); - Object customValidator2 = this.context.getBean("customValidator2"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isNotSameAs(customValidator1) - .isNotSameAs(customValidator2); - assertThat(springValidatorBeans).containsExactly("customValidator1", - "customValidator2", "mvcValidator"); - assertThat(jsrValidatorBeans).isEmpty(); - } - - @Test - public void validatorWhenMultipleValidatorsAndMvcValidatorShouldUseMvc() - throws Exception { - load(MultipleValidatorsAndMvcValidator.class); - Object customValidator = this.context.getBean("customValidator"); - Object mvcValidator = this.context.getBean("mvcValidator"); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(mvcValidator).isNotSameAs(customValidator); - assertThat(springValidatorBeans).containsExactly("customValidator", - "mvcValidator"); - assertThat(jsrValidatorBeans).isEmpty(); + public void validationJsr303ValidatorExposedAsSpringValidator() { + load(Jsr303Validator.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); + 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)); } 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, - ValidationAutoConfiguration.class, WebMvcAutoConfiguration.class, + configClasses.addAll(Arrays.asList(Config.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(); } @@ -974,88 +895,47 @@ public class WebMvcAutoConfigurationTests { } @Configuration - protected static class ValidatorWebMvcConfigurer extends WebMvcConfigurerAdapter { + protected static class MvcValidator extends WebMvcConfigurerAdapter { + + private final Validator validator = mock(Validator.class); @Override public Validator getValidator() { - return mock(Validator.class); + return this.validator; } } @Configuration - static class UserDefinedSpringOnlyValidator { + protected static class MvcJsr303Validator extends WebMvcConfigurerAdapter { - @Bean - public Validator customValidator() { - return mock(Validator.class); + private final LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + + @Override + public Validator getValidator() { + return this.validator; } } @Configuration - static class UserDefinedJsr303Validator { + static class Jsr303Validator { @Bean - public javax.validation.Validator customValidator() { + public javax.validation.Validator jsr303Validator() { return mock(javax.validation.Validator.class); } } @Configuration - static class UserDefinedSingleJsr303AndSpringValidator { + static class CustomValidator { @Bean - public LocalValidatorFactoryBean customValidator() { + public Validator customValidator() { return new LocalValidatorFactoryBean(); } } - @Configuration - static class UserDefinedJsr303AndSpringValidator { - - @Bean - public javax.validation.Validator customJsrValidator() { - return mock(javax.validation.Validator.class); - } - - @Bean - public Validator customSpringValidator() { - return mock(Validator.class); - } - - } - - @Configuration - static class MultipleValidatorsAndNoMvcValidator { - - @Bean - public Validator customValidator1() { - return mock(Validator.class); - } - - @Bean - public Validator customValidator2() { - return mock(Validator.class); - } - - } - - @Configuration - static class MultipleValidatorsAndMvcValidator { - - @Bean - public Validator customValidator() { - return mock(Validator.class); - } - - @Bean - public Validator mvcValidator() { - return mock(Validator.class); - } - - } - } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcValidatorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcValidatorTests.java new file mode 100644 index 0000000000..a0df0cf239 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcValidatorTests.java @@ -0,0 +1,152 @@ +/* + * 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.web; + +import java.util.HashMap; + +import javax.validation.constraints.Min; + +import org.junit.After; +import org.junit.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.MapBindingResult; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link WebMvcValidator}. + * + * @author Stephane Nicoll + */ +public class WebMvcValidatorTests { + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void wrapLocalValidatorFactoryBean() { + WebMvcValidator wrapper = load( + LocalValidatorFactoryBeanConfig.class); + assertThat(wrapper.supports(SampleData.class)).isTrue(); + MapBindingResult errors = new MapBindingResult(new HashMap(), + "test"); + wrapper.validate(new SampleData(40), errors); + assertThat(errors.getErrorCount()).isEqualTo(1); + } + + @Test + public void wrapperInvokesCallbackOnNonManagedBean() { + load(NonManagedBeanConfig.class); + LocalValidatorFactoryBean validator = this.context + .getBean(NonManagedBeanConfig.class).validator; + verify(validator, times(1)).setApplicationContext(any(ApplicationContext.class)); + verify(validator, times(1)).afterPropertiesSet(); + verify(validator, times(0)).destroy(); + this.context.close(); + this.context = null; + verify(validator, times(1)).destroy(); + } + + @Test + public void wrapperDoesNotInvokeCallbackOnManagedBean() { + load(ManagedBeanConfig.class); + LocalValidatorFactoryBean validator = this.context + .getBean(ManagedBeanConfig.class).validator; + verify(validator, times(0)).setApplicationContext(any(ApplicationContext.class)); + verify(validator, times(0)).afterPropertiesSet(); + verify(validator, times(0)).destroy(); + this.context.close(); + this.context = null; + verify(validator, times(0)).destroy(); + } + + private WebMvcValidator load(Class config) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(config); + ctx.refresh(); + this.context = ctx; + return this.context.getBean(WebMvcValidator.class); + } + + @Configuration + static class LocalValidatorFactoryBeanConfig { + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + + @Bean + public WebMvcValidator wrapper() { + return new WebMvcValidator(validator(), true); + } + + } + + @Configuration + static class NonManagedBeanConfig { + + private final LocalValidatorFactoryBean validator = mock( + LocalValidatorFactoryBean.class); + + @Bean + public WebMvcValidator wrapper() { + return new WebMvcValidator(this.validator, false); + } + + } + + @Configuration + static class ManagedBeanConfig { + + private final LocalValidatorFactoryBean validator = mock( + LocalValidatorFactoryBean.class); + + @Bean + public WebMvcValidator wrapper() { + return new WebMvcValidator(this.validator, true); + } + + } + + static class SampleData { + + @Min(42) + private int counter; + + SampleData(int counter) { + this.counter = counter; + } + + } + +} From 1de2316a0b4c2d5896c34b954208a4c550466f22 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 25 Apr 2017 22:28:56 +0200 Subject: [PATCH 2/2] Refine validator and MVC validator configuration This commit ensures that a primary JSR 303 and Spring Validator will be exposed if the auto-configuration kicks in. As `LocalValidatorFactoryBean` exposes 3 contracts (JSR-303 `Validator` and `ValidatorFactory` as well as the `Spring` validator one), this makes sure that those types can be injected by type. `LocalValidatorFactoryBean` exposes 3 contracts and we're only checking for the absence of a `javax.validation.Validator` to auto-configure a `LocalValidatorFactoryBean`. If no standard JSR validator exists but a Spring's `Validator` exists and is primary, we shouldn't flag the auto-configured one as `@Primary`. Previous iterations on this feature have made sure that we'll auto-configure at most one `javax.validation.Validator` so not flagging it `@Primary` is no problem. This commit also restores and adds tests that validates `ValidationAutoConfiguration` will configure a JSR validator even if a Spring Validator is present. This effectively fixes gh-8495 in a different way. Closes gh-8979 Closes gh-8976 --- .../PrimaryDefaultValidatorPostProcessor.java | 88 ++++++++++ .../ValidationAutoConfiguration.java | 6 +- .../ValidationAutoConfigurationTests.java | 166 +++++++++++++++++- .../web/WebMvcAutoConfigurationTests.java | 113 ++++++++---- 4 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java 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); - Validator validator = this.context.getBean("mvcValidator", Validator.class); - assertThat(validator).isInstanceOf(WebMvcValidator.class); - assertThat(((WebMvcValidator) validator).getTarget()) - .isSameAs(this.context.getBean(javax.validation.Validator.class)); + 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 validationJsr303ValidatorExposedAsSpringValidator() { - load(Jsr303Validator.class); + 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(defaultValidator); + // Primary Spring validator is the one use by MVC behind the scenes + assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator); + } + + @Test + 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); } }