From c9561f031c591ca55e028ca66250511c5e62002a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 Apr 2017 22:32:02 -0700 Subject: [PATCH] Refine validator and MVC validator configuration Update `ValidationAutoConfiguration` and `WebMvcAutoConfiguration` to ensure as much as possible that only a single Validator bean of each type is registered. Validation auto-configuration now does the following: - If no validator is found: Registers a `LocalValidatorFactoryBean` (providing both Spring and JSR validation) - If the user defines a Spring & JSR validator: Backs off - If the user defines only a JSR validator: Adapts it to a Spring validator (without exposing another JSR implementation) WebMvcAutoConfiguration auto-configuration has been updated to make MVC validation follow common Spring Boot patterns: - If not validator beans are found (due to the user excluding ValidationAutoConfiguration) a new `mvcValidator` bean will be registered. - If a single validator bean is found it will be used for MVC validation. - If multiple validator beans are defined it will either use the one named `mvcValidator` or it will register a new `mvcValidator` bean Any automatically registered `mvcValidator` bean will not implement the JSR validator interface. Finally, it is no longer possible to provide an MVC validator via a `WebMvcConfigurer`. Fixes gh-8495 --- .../EndpointMvcIntegrationTests.java | 7 +- .../DefaultValidatorConfiguration.java | 47 ++++ .../validation/DelegatingValidator.java | 78 ++++++ .../Jsr303ValidatorAdapterConfiguration.java | 46 ++++ .../ValidationAutoConfiguration.java | 19 +- .../web/WebMvcAutoConfiguration.java | 178 ++++++++++++- .../autoconfigure/web/WebMvcValidator.java | 144 ----------- ...ringBootWebSecurityConfigurationTests.java | 7 +- .../validation/DelegatingValidatorTests.java | 117 +++++++++ .../ValidationAutoConfigurationTests.java | 152 ++++++++--- ...asicErrorControllerDirectMockMvcTests.java | 7 +- .../web/BasicErrorControllerMockMvcTests.java | 7 +- .../web/WebMvcAutoConfigurationTests.java | 244 +++++++++++++----- .../web/WebMvcValidatorTests.java | 152 ----------- 14 files changed, 774 insertions(+), 431 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DefaultValidatorConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/Jsr303ValidatorAdapterConfiguration.java delete mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcValidator.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java delete 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 d6312f7dc34..146b99e4b40 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,6 +41,7 @@ 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; @@ -156,9 +157,9 @@ public class EndpointMvcIntegrationTests { @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - JacksonAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.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 new file mode 100644 index 00000000000..9311a272c6f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DefaultValidatorConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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 new file mode 100644 index 00000000000..dc9fc725b6b --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java @@ -0,0 +1,78 @@ +/* + * 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); + } + } + + 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 new file mode 100644 index 00000000000..2573dd3e3cf --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/Jsr303ValidatorAdapterConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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 9feba19dad6..99c4efa6b9b 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,18 +19,16 @@ 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.Role; +import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; /** @@ -43,19 +41,12 @@ 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 - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - @ConditionalOnMissingBean - public static Validator jsr303Validator() { - LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); - MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); - factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); - return factoryBean; - } - - @Bean + @ConditionalOnBean(Validator.class) @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 b96fc8b152b..f08693eaca0 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,11 +30,21 @@ 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.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.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +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; @@ -43,27 +53,38 @@ 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.Primary; +import org.springframework.context.annotation.Role; import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; 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.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; @@ -142,6 +163,12 @@ public class WebMvcAutoConfiguration { public static final String SKIP_PATH_EXTENSION_CONTENT_NEGOTIATION_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class .getName() + ".SKIP"; + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public static MvcValidatorPostProcessor mvcValidatorAliasPostProcessor() { + return new MvcValidatorPostProcessor(); + } + @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { @@ -367,21 +394,22 @@ public class WebMvcAutoConfiguration { * Configuration equivalent to {@code @EnableWebMvc}. */ @Configuration - public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { + public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration + implements InitializingBean { private final WebMvcProperties mvcProperties; - private final ListableBeanFactory beanFactory; + private final ApplicationContext context; private final WebMvcRegistrations mvcRegistrations; public EnableWebMvcConfiguration( ObjectProvider mvcPropertiesProvider, ObjectProvider mvcRegistrationsProvider, - ListableBeanFactory beanFactory) { + ApplicationContext context) { this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); - this.beanFactory = beanFactory; + this.context = context; } @Bean @@ -412,12 +440,9 @@ public class WebMvcAutoConfiguration { @Bean @Override + @Conditional(DisableMvcValidatorCondition.class) public Validator mvcValidator() { - if (!ClassUtils.isPresent("javax.validation.Validator", - getClass().getClassLoader())) { - return super.mvcValidator(); - } - return WebMvcValidator.get(getApplicationContext(), getValidator()); + return this.context.getBean("mvcValidator", Validator.class); } @Override @@ -432,7 +457,7 @@ public class WebMvcAutoConfiguration { @Override protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() { try { - return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class); + return this.context.getBean(ConfigurableWebBindingInitializer.class); } catch (NoSuchBeanDefinitionException ex) { return super.getConfigurableWebBindingInitializer(); @@ -481,6 +506,15 @@ 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 @@ -606,4 +640,128 @@ public class WebMvcAutoConfiguration { } + /** + * Condition used to disable the default MVC validator registration. The + * {@link MvcValidatorPostProcessor} is used to configure 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 BeanFactoryPostProcessor} 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.
  • + *
+ */ + @Order(Ordered.LOWEST_PRECEDENCE) + static class MvcValidatorPostProcessor + implements BeanDefinitionRegistryPostProcessor { + + private static final String JSR303_VALIDATOR_CLASS = "javax.validation.Validator"; + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + if (registry instanceof ListableBeanFactory) { + postProcess(registry, (ListableBeanFactory) registry); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + } + + private void postProcess(BeanDefinitionRegistry registry, + ListableBeanFactory beanFactory) { + String[] validatorBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + beanFactory, Validator.class, false, false); + if (validatorBeans.length == 0) { + registerMvcValidator(registry, beanFactory); + } + else if (validatorBeans.length == 1) { + registry.registerAlias(validatorBeans[0], "mvcValidator"); + } + else { + if (!ObjectUtils.containsElement(validatorBeans, "mvcValidator")) { + registerMvcValidator(registry, beanFactory); + } + } + } + + private void registerMvcValidator(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 { + + public 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 deleted file mode 100644 index 499e08dbfdd..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcValidator.java +++ /dev/null @@ -1,144 +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.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 07812e28911..b8d3fddfa16 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,6 +31,7 @@ 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; @@ -324,9 +325,9 @@ public class SpringBootWebSecurityConfigurationTests { @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.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 new file mode 100644 index 00000000000..3ced1d921e9 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java @@ -0,0 +1,117 @@ +/* + * 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 1ea207316ca..df2e7e3cd9e 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,9 +32,12 @@ 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}. @@ -56,45 +59,94 @@ public class ValidationAutoConfigurationTests { } @Test - public void validationIsEnabled() { - load(SampleService.class); + 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); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - SampleService service = this.context.getBean(SampleService.class); - service.doSomething("Valid"); + ClassWithConstraint service = this.context.getBean(ClassWithConstraint.class); + service.call("Valid"); this.thrown.expect(ConstraintViolationException.class); - service.doSomething("KO"); + service.call("KO"); } @Test - public void validationUsesCglibProxy() { - load(DefaultAnotherSampleService.class); + public void validationAutoConfigurationShouldUseCglibProxy() { + load(ImplementationOfInterfaceWithConstraint.class); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - DefaultAnotherSampleService service = this.context - .getBean(DefaultAnotherSampleService.class); - service.doSomething(42); + ImplementationOfInterfaceWithConstraint service = this.context + .getBean(ImplementationOfInterfaceWithConstraint.class); + service.call(42); this.thrown.expect(ConstraintViolationException.class); - service.doSomething(2); + service.call(2); } @Test - public void validationCanBeConfiguredToUseJdkProxy() { + public void validationAutoConfigurationWhenProxyTargetClassIsFalseShouldUseJdkProxy() { load(AnotherSampleServiceConfiguration.class, "spring.aop.proxy-target-class=false"); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - assertThat(this.context.getBeansOfType(DefaultAnotherSampleService.class)) - .isEmpty(); - AnotherSampleService service = this.context.getBean(AnotherSampleService.class); - service.doSomething(42); + assertThat(this.context + .getBeansOfType(ImplementationOfInterfaceWithConstraint.class)).isEmpty(); + InterfaceWithConstraint service = this.context + .getBean(InterfaceWithConstraint.class); + service.call(42); this.thrown.expect(ConstraintViolationException.class); - service.doSomething(2); + service.call(2); } @Test - public void userDefinedMethodValidationPostProcessorTakesPrecedence() { - load(SampleConfiguration.class); + public void validationAutoConfigurationWhenUserDefinesMethodValidationPostProcessorShouldBackOff() { + load(UserDefinedMethodValidationConfig.class); assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); Object userMethodValidationPostProcessor = this.context - .getBean("testMethodValidationPostProcessor"); + .getBean("customMethodValidationPostProcessor"); assertThat(this.context.getBean(MethodValidationPostProcessor.class)) .isSameAs(userMethodValidationPostProcessor); assertThat(this.context.getBeansOfType(MethodValidationPostProcessor.class)) @@ -115,47 +167,73 @@ public class ValidationAutoConfigurationTests { this.context = ctx; } - @Validated - static class SampleService { + @Configuration + static class Config { - public void doSomething(@Size(min = 3, max = 10) String name) { + } + + @Configuration + static class UserDefinedValidatorConfig { + @Bean + public OptionalValidatorFactoryBean customValidator() { + return new OptionalValidatorFactoryBean(); } } - interface AnotherSampleService { + @Configuration + static class UserDefinedJsrValidatorConfig { + + @Bean + public Validator customValidator() { + return mock(Validator.class); + } - void doSomething(@Min(42) Integer counter); } - @Validated - static class DefaultAnotherSampleService implements AnotherSampleService { - - @Override - public void doSomething(Integer counter) { + @Configuration + static class UserDefinedMethodValidationConfig { + @Bean + public MethodValidationPostProcessor customMethodValidationPostProcessor() { + return new MethodValidationPostProcessor(); } + } @Configuration static class AnotherSampleServiceConfiguration { @Bean - public AnotherSampleService anotherSampleService() { - return new DefaultAnotherSampleService(); + public InterfaceWithConstraint implementationOfInterfaceWithConstraint() { + return new ImplementationOfInterfaceWithConstraint(); } } - @Configuration - static class SampleConfiguration { + @Validated + static class ClassWithConstraint { + + public void call(@Size(min = 3, max = 10) String name) { - @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 4879481093a..29bb265dbdc 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,6 +35,7 @@ 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; @@ -126,9 +127,9 @@ public class BasicErrorControllerDirectMockMvcTests { @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.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 3ed4e8a9a0c..da5a0c5820c 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,6 +35,7 @@ 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; @@ -128,9 +129,9 @@ public class BasicErrorControllerMockMvcTests { @Import({ EmbeddedServletContainerAutoConfiguration.EmbeddedTomcat.class, EmbeddedServletContainerAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.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 724486d37d5..ca0355ce42e 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,7 +27,6 @@ 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; @@ -37,8 +36,11 @@ 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; @@ -59,11 +61,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; @@ -655,77 +657,154 @@ public class WebMvcAutoConfigurationTests { } @Test - public void validationNoJsr303ValidatorExposedByDefault() { + 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 { load(); - assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); - assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) - .isEmpty(); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); + 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"); } @Test - 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); + 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(); } @Test - 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); + 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"); } @Test - 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)); + 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"); } @Test - 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)); + 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(); } 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(); } @@ -895,45 +974,86 @@ public class WebMvcAutoConfigurationTests { } @Configuration - protected static class MvcValidator extends WebMvcConfigurerAdapter { - - private final Validator validator = mock(Validator.class); + protected static class ValidatorWebMvcConfigurer extends WebMvcConfigurerAdapter { @Override public Validator getValidator() { - return this.validator; + return mock(Validator.class); } } @Configuration - protected static class MvcJsr303Validator extends WebMvcConfigurerAdapter { + static class UserDefinedSpringOnlyValidator { - private final LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + @Bean + public Validator customValidator() { + return mock(Validator.class); + } - @Override - public Validator getValidator() { - return this.validator; + } + + @Configuration + static class UserDefinedJsr303Validator { + + @Bean + public javax.validation.Validator customValidator() { + return mock(javax.validation.Validator.class); } } @Configuration - static class Jsr303Validator { + static class UserDefinedSingleJsr303AndSpringValidator { @Bean - public javax.validation.Validator jsr303Validator() { + public LocalValidatorFactoryBean 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 CustomValidator { + 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 new LocalValidatorFactoryBean(); + 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 deleted file mode 100644 index a0df0cf2393..00000000000 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebMvcValidatorTests.java +++ /dev/null @@ -1,152 +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.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; - } - - } - -}