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 95b3b5b5bca..cf56833f016 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 @@ -42,6 +42,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -85,15 +86,16 @@ public class EndpointMvcIntegrationTests { @Test public void envEndpointNotHidden() throws InterruptedException { String body = new TestRestTemplate().getForObject( - "http://localhost:" + this.port + "/application/env/foo.bar", String.class); + "http://localhost:" + this.port + "/application/env/foo.bar", + String.class); assertThat(body).isNotNull().contains("\"baz\""); assertThat(this.interceptor.invoked()).isTrue(); } @Test public void healthEndpointNotHidden() throws InterruptedException { - String body = new TestRestTemplate() - .getForObject("http://localhost:" + this.port + "/application/health", String.class); + String body = new TestRestTemplate().getForObject( + "http://localhost:" + this.port + "/application/health", String.class); assertThat(body).isNotNull().contains("status"); assertThat(this.interceptor.invoked()).isTrue(); } @@ -153,9 +155,9 @@ public class EndpointMvcIntegrationTests { @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ ServletWebServerFactoryAutoConfiguration.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..e180ba284a4 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/DelegatingValidator.java @@ -0,0 +1,82 @@ +/* + * 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 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/SpringValidator.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/SpringValidator.java deleted file mode 100644 index 75dcf08562b..00000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/SpringValidator.java +++ /dev/null @@ -1,145 +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.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 - * @since 2.0.0 - */ -public class SpringValidator implements SmartValidator, ApplicationContextAware, - InitializingBean, DisposableBean { - - private final SpringValidatorAdapter target; - - private final boolean existingBean; - - public SpringValidator(SpringValidatorAdapter target, boolean existingBean) { - this.target = target; - this.existingBean = existingBean; - } - - public final 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 SpringValidator((SpringValidatorAdapter) validator, - existingBean); - } - return new SpringValidator( - new SpringValidatorAdapter((javax.validation.Validator) validator), - existingBean); - } - return 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/reactive/WebFluxAnnotationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAnnotationAutoConfiguration.java index 90e16045380..f690ccf66a7 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAnnotationAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAnnotationAutoConfiguration.java @@ -23,30 +23,50 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +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.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; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.validation.SpringValidator; +import org.springframework.boot.autoconfigure.validation.DelegatingValidator; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +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.Role; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.http.CacheControl; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.EnableWebFlux; @@ -82,6 +102,12 @@ import org.springframework.web.reactive.result.view.ViewResolver; @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebFluxAnnotationAutoConfiguration { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public static WebFluxValidatorPostProcessor mvcValidatorAliasPostProcessor() { + return new WebFluxValidatorPostProcessor(); + } + @Configuration @EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class }) @Import(EnableWebFluxConfiguration.class) @@ -190,17 +216,29 @@ public class WebFluxAnnotationAutoConfiguration { * Configuration equivalent to {@code @EnableWebFlux}. */ @Configuration - public static class EnableWebFluxConfiguration - extends DelegatingWebFluxConfiguration { + public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration + implements InitializingBean { + + private final ApplicationContext context; + + public EnableWebFluxConfiguration(ApplicationContext context) { + this.context = context; + } + + @Bean + @Override + @Conditional(DisableWebFluxValidatorCondition.class) + public Validator webFluxValidator() { + return this.context.getBean("webFluxValidator", Validator.class); + } @Override - @Bean - public Validator webFluxValidator() { - if (!ClassUtils.isPresent("javax.validation.Validator", - getClass().getClassLoader())) { - return super.webFluxValidator(); - } - return SpringValidator.get(getApplicationContext(), getValidator()); + public void afterPropertiesSet() throws Exception { + Assert.state(getValidator() == null, + "Found unexpected validator configuration. A Spring Boot WebFlux " + + "validator should be registered as bean named " + + "'webFluxValidator' and not returned from " + + "WebFluxConfigurer.getValidator()"); } } @@ -266,4 +304,128 @@ public class WebFluxAnnotationAutoConfiguration { } + /** + * Condition used to disable the default WebFlux validator registration. The + * {@link WebFluxValidatorPostProcessor} is used to configure the + * {@code webFluxValidator} bean. + */ + static class DisableWebFluxValidatorCondition 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: + * + */ + @Order(Ordered.LOWEST_PRECEDENCE) + static class WebFluxValidatorPostProcessor + 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], "webFluxValidator"); + } + else { + if (!ObjectUtils.containsElement(validatorBeans, "webFluxValidator")) { + registerMvcValidator(registry, beanFactory); + } + } + } + + private void registerMvcValidator(BeanDefinitionRegistry registry, + ListableBeanFactory beanFactory) { + RootBeanDefinition definition = new RootBeanDefinition(); + definition.setBeanClass(getClass()); + definition.setFactoryMethodName("webFluxValidator"); + registry.registerBeanDefinition("webFluxValidator", definition); + } + + static Validator webFluxValidator() { + Validator validator = new WebFluxConfigurationSupport().webFluxValidator(); + try { + if (ClassUtils.forName(JSR303_VALIDATOR_CLASS, null) + .isInstance(validator)) { + return new DelegatingWebFluxValidator(validator); + } + } + catch (Exception ex) { + } + return validator; + } + + } + + /** + * {@link DelegatingValidator} for the WebFlux validator. + */ + static class DelegatingWebFluxValidator extends DelegatingValidator + implements ApplicationContextAware, InitializingBean, DisposableBean { + + DelegatingWebFluxValidator(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/servlet/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 6399bdccce9..a9e0f49db66 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/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; @@ -45,7 +55,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; -import org.springframework.boot.autoconfigure.validation.SpringValidator; +import org.springframework.boot.autoconfigure.validation.DelegatingValidator; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ResourceProperties; @@ -54,21 +64,31 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter; import org.springframework.boot.web.servlet.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; @@ -147,6 +167,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() { @@ -372,21 +398,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 @@ -417,12 +444,9 @@ public class WebMvcAutoConfiguration { @Bean @Override + @Conditional(DisableMvcValidatorCondition.class) public Validator mvcValidator() { - if (!ClassUtils.isPresent("javax.validation.Validator", - getClass().getClassLoader())) { - return super.mvcValidator(); - } - return SpringValidator.get(getApplicationContext(), getValidator()); + return this.context.getBean("mvcValidator", Validator.class); } @Override @@ -437,7 +461,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(); @@ -486,6 +510,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 @@ -611,4 +644,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: + * + */ + @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 { + + 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/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfigurationTests.java index ce9b93a3c6c..0f68897c967 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 @@ -32,6 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -322,9 +323,9 @@ public class SpringBootWebSecurityConfigurationTests { @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ ServletWebServerFactoryAutoConfiguration.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..04cc4e8c075 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java @@ -0,0 +1,116 @@ +/* + * 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/SpringValidatorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/SpringValidatorTests.java deleted file mode 100644 index cb9a5bcc434..00000000000 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/SpringValidatorTests.java +++ /dev/null @@ -1,151 +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 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.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link SpringValidator}. - * - * @author Stephane Nicoll - */ -public class SpringValidatorTests { - - private AnnotationConfigApplicationContext context; - - @After - public void close() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void wrapLocalValidatorFactoryBean() { - SpringValidator 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 SpringValidator load(Class config) { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(config); - ctx.refresh(); - this.context = ctx; - return this.context.getBean(SpringValidator.class); - } - - @Configuration - static class LocalValidatorFactoryBeanConfig { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } - - @Bean - public SpringValidator wrapper() { - return new SpringValidator(validator(), true); - } - - } - - @Configuration - static class NonManagedBeanConfig { - - private final LocalValidatorFactoryBean validator = mock( - LocalValidatorFactoryBean.class); - - @Bean - public SpringValidator wrapper() { - return new SpringValidator(this.validator, false); - } - - } - - @Configuration - static class ManagedBeanConfig { - - private final LocalValidatorFactoryBean validator = mock( - LocalValidatorFactoryBean.class); - - @Bean - public SpringValidator wrapper() { - return new SpringValidator(this.validator, true); - } - - } - - static class SampleData { - - @Min(42) - private int counter; - - SampleData(int counter) { - this.counter = counter; - } - - } - -} 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); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - SampleService service = this.context.getBean(SampleService.class); - service.doSomething("Valid"); - this.thrown.expect(ConstraintViolationException.class); - service.doSomething("KO"); + 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 validationUsesCglibProxy() { - load(DefaultAnotherSampleService.class); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - DefaultAnotherSampleService service = this.context - .getBean(DefaultAnotherSampleService.class); - service.doSomething(42); - this.thrown.expect(ConstraintViolationException.class); - service.doSomething(2); + 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 validationCanBeConfiguredToUseJdkProxy() { + 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); + ClassWithConstraint service = this.context.getBean(ClassWithConstraint.class); + service.call("Valid"); + this.thrown.expect(ConstraintViolationException.class); + service.call("KO"); + } + + @Test + public void validationAutoConfigurationShouldUseCglibProxy() { + load(ImplementationOfInterfaceWithConstraint.class); + assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); + ImplementationOfInterfaceWithConstraint service = this.context + .getBean(ImplementationOfInterfaceWithConstraint.class); + service.call(42); + this.thrown.expect(ConstraintViolationException.class); + service.call(2); + } + + @Test + 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/reactive/WebFluxAnnotationAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAnnotationAutoConfigurationTests.java index e3d6820ec62..91dbee202b4 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAnnotationAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAnnotationAutoConfigurationTests.java @@ -16,15 +16,19 @@ package org.springframework.boot.autoconfigure.web.reactive; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; -import javax.validation.ValidatorFactory; - +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.boot.autoconfigure.validation.SpringValidator; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.autoconfigure.validation.DelegatingValidator; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.Config; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; @@ -36,9 +40,9 @@ import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ObjectUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.validation.beanvalidation.SpringValidatorAdapter; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.CompositeContentTypeResolver; import org.springframework.web.reactive.config.WebFluxConfigurationSupport; @@ -65,6 +69,9 @@ import static org.mockito.Mockito.mock; */ public class WebFluxAnnotationAutoConfigurationTests { + @Rule + public ExpectedException thrown = ExpectedException.none(); + private GenericReactiveWebApplicationContext context; @Test @@ -165,64 +172,134 @@ public class WebFluxAnnotationAutoConfigurationTests { } @Test - public void validationNoJsr303ValidatorExposedByDefault() { + public void validatorWhenSuppliedByConfigurerShouldThrowException() throws Exception { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("unexpected validator configuration"); + load(ValidatorWebFluxConfigurer.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 webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).isSameAs(defaultValidator); + assertThat(springValidatorBeans).containsExactly("defaultValidator"); + assertThat(jsrValidatorBeans).containsExactly("defaultValidator"); } @Test - public void validationCustomConfigurerTakesPrecedence() { - load(WebFluxValidator.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(WebFluxValidator.class).validator); + public void validatorWhenUserDefinedSpringOnlyShouldUseDefined() throws Exception { + load(UserDefinedSpringOnlyValidator.class); + Object customValidator = this.context.getBean("customValidator"); + Object webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).isSameAs(customValidator); + assertThat(this.context.getBean(Validator.class)).isEqualTo(customValidator); + assertThat(springValidatorBeans).containsExactly("customValidator"); + assertThat(jsrValidatorBeans).isEmpty(); } @Test - public void validationCustomConfigurerTakesPrecedenceAndDoNotExposeJsr303() { - load(WebFluxJsr303Validator.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(SpringValidator.class); - assertThat(((SpringValidator) validator).getTarget()) - .isSameAs(this.context.getBean(WebFluxJsr303Validator.class).validator); + public void validatorWhenUserDefinedJsr303ShouldAdapt() throws Exception { + load(UserDefinedJsr303Validator.class); + Object customValidator = this.context.getBean("customValidator"); + Object webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).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("webFluxValidator", Validator.class); - assertThat(validator).isInstanceOf(SpringValidator.class); - assertThat(((SpringValidator) 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 webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).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(SpringValidator.class); - SpringValidatorAdapter target = ((SpringValidator) 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 webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(customJsrValidator).isNotSameAs(customSpringValidator); + assertThat(webFluxValidator).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 webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).isInstanceOf(DelegatingValidator.class); + assertThat(springValidatorBeans).containsExactly("webFluxValidator"); + assertThat(jsrValidatorBeans).isEmpty(); + } + + @Test + public void validatorWhenMultipleValidatorsAndNoWebFluxValidatorShouldAddMvc() + throws Exception { + load(MultipleValidatorsAndNoWebFluxValidator.class); + Object customValidator1 = this.context.getBean("customValidator1"); + Object customValidator2 = this.context.getBean("customValidator2"); + Object webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).isNotSameAs(customValidator1) + .isNotSameAs(customValidator2); + assertThat(springValidatorBeans).containsExactly("customValidator1", + "customValidator2", "webFluxValidator"); + assertThat(jsrValidatorBeans).isEmpty(); + } + + @Test + public void validatorWhenMultipleValidatorsAndWebFluxValidatorShouldUseMvc() + throws Exception { + load(MultipleValidatorsAndWebFluxValidator.class); + Object customValidator = this.context.getBean("customValidator"); + Object webFluxValidator = this.context.getBean("webFluxValidator"); + String[] jsrValidatorBeans = this.context + .getBeanNamesForType(javax.validation.Validator.class); + String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); + assertThat(webFluxValidator).isNotSameAs(customValidator); + assertThat(springValidatorBeans).containsExactly("customValidator", + "webFluxValidator"); + assertThat(jsrValidatorBeans).isEmpty(); } private void load(String... environment) { @@ -230,13 +307,24 @@ public class WebFluxAnnotationAutoConfigurationTests { } private void load(Class config, String... environment) { + load(config, null, environment); + } + + private void load(Class config, Class[] exclude, String... environment) { this.context = new GenericReactiveWebApplicationContext(); EnvironmentTestUtils.addEnvironment(this.context, environment); + List> configClasses = new ArrayList<>(); if (config != null) { - this.context.register(config); + configClasses.add(config); } - this.context.register(BaseConfiguration.class); + configClasses.addAll(Arrays.asList(Config.class, + ValidationAutoConfiguration.class, BaseConfiguration.class)); + if (!ObjectUtils.isEmpty(exclude)) { + configClasses.removeAll(Arrays.asList(exclude)); + } + this.context.register(configClasses.toArray(new Class[configClasses.size()])); this.context.refresh(); + } @Configuration @@ -291,47 +379,88 @@ public class WebFluxAnnotationAutoConfigurationTests { } @Configuration - protected static class WebFluxValidator implements WebFluxConfigurer { - - private final Validator validator = mock(Validator.class); + protected static class ValidatorWebFluxConfigurer implements WebFluxConfigurer { @Override public Optional getValidator() { - return Optional.of(this.validator); + return Optional.of(mock(Validator.class)); } } @Configuration - protected static class WebFluxJsr303Validator implements WebFluxConfigurer { - - private final LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - - @Override - public Optional getValidator() { - return Optional.of(this.validator); - } - - } - - @Configuration - static class Jsr303Validator { + static class UserDefinedSpringOnlyValidator { @Bean - public javax.validation.Validator jsr303Validator() { + public Validator customValidator() { + return mock(Validator.class); + } + + } + + @Configuration + static class UserDefinedJsr303Validator { + + @Bean + public javax.validation.Validator customValidator() { return mock(javax.validation.Validator.class); } } @Configuration - static class CustomValidator { + static class UserDefinedSingleJsr303AndSpringValidator { @Bean - public Validator customValidator() { + 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 MultipleValidatorsAndNoWebFluxValidator { + + @Bean + public Validator customValidator1() { + return mock(Validator.class); + } + + @Bean + public Validator customValidator2() { + return mock(Validator.class); + } + + } + + @Configuration + static class MultipleValidatorsAndWebFluxValidator { + + @Bean + public Validator customValidator() { + return mock(Validator.class); + } + + @Bean + public Validator webFluxValidator() { + return mock(Validator.class); + } + + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 2fdca55954a..f1d39693974 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/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,11 +36,13 @@ 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.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.validation.SpringValidator; +import org.springframework.boot.autoconfigure.validation.DelegatingValidator; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WelcomePageHandlerMapping; import org.springframework.boot.test.util.EnvironmentTestUtils; @@ -62,11 +63,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; @@ -658,76 +659,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(SpringValidator.class); - assertThat(((SpringValidator) 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(SpringValidator.class); - assertThat(((SpringValidator) 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(SpringValidator.class); - SpringValidatorAdapter target = ((SpringValidator) 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 AnnotationConfigServletWebServerApplicationContext(); 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(); } @@ -897,47 +976,88 @@ 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 { - - private final LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - - @Override - public Validator getValidator() { - return this.validator; - } - - } - - @Configuration - static class Jsr303Validator { + static class UserDefinedSpringOnlyValidator { @Bean - public javax.validation.Validator jsr303Validator() { + public Validator customValidator() { + return mock(Validator.class); + } + + } + + @Configuration + static class UserDefinedJsr303Validator { + + @Bean + public javax.validation.Validator customValidator() { return mock(javax.validation.Validator.class); } } @Configuration - static class CustomValidator { + static class UserDefinedSingleJsr303AndSpringValidator { @Bean - public Validator customValidator() { + 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 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/servlet/error/BasicErrorControllerDirectMockMvcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerDirectMockMvcTests.java index 255d07e773e..ce092308ece 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerDirectMockMvcTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerDirectMockMvcTests.java @@ -36,6 +36,7 @@ import org.junit.rules.ExpectedException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -129,9 +130,9 @@ public class BasicErrorControllerDirectMockMvcTests { @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ ServletWebServerFactoryAutoConfiguration.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/servlet/error/BasicErrorControllerMockMvcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerMockMvcTests.java index 881dc9cf32e..45e451a9a5e 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerMockMvcTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerMockMvcTests.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -131,9 +132,9 @@ public class BasicErrorControllerMockMvcTests { @Documented @Import({ ServletWebServerFactoryAutoConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class }) + DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class, + WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) private @interface MinimalWebConfiguration { } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/options/OptionSetGroovyCompilerConfiguration.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/options/OptionSetGroovyCompilerConfiguration.java index b3e0ee79194..d4a6b2ec19c 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/options/OptionSetGroovyCompilerConfiguration.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/options/OptionSetGroovyCompilerConfiguration.java @@ -92,4 +92,9 @@ public class OptionSetGroovyCompilerConfiguration implements GroovyCompilerConfi return this.repositoryConfiguration; } + @Override + public boolean isQuiet() { + return false; + } + } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/run/RunCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/run/RunCommand.java index 97b32f30cc9..ed480ed6f60 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/run/RunCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/run/RunCommand.java @@ -140,7 +140,7 @@ public class RunCommand extends OptionParsingCommand { @Override public Level getLogLevel() { - if (getOptions().has(RunOptionHandler.this.quietOption)) { + if (isQuiet()) { return Level.OFF; } if (getOptions().has(RunOptionHandler.this.verboseOption)) { @@ -149,6 +149,11 @@ public class RunCommand extends OptionParsingCommand { return Level.INFO; } + @Override + public boolean isQuiet() { + return getOptions().has(RunOptionHandler.this.quietOption); + } + } } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java index 0e31ed167fc..9f9172455d1 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java @@ -95,7 +95,8 @@ public class GroovyCompiler { new SpringBootDependenciesDependencyManagement()); AetherGrapeEngine grapeEngine = AetherGrapeEngineFactory.create(this.loader, - configuration.getRepositoryConfiguration(), resolutionContext); + configuration.getRepositoryConfiguration(), resolutionContext, + configuration.isQuiet()); GrapeEngineInstaller.install(grapeEngine); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerConfiguration.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerConfiguration.java index 7741f3242cf..7fd8462dc86 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerConfiguration.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerConfiguration.java @@ -71,4 +71,10 @@ public interface GroovyCompilerConfiguration { */ List getRepositoryConfiguration(); + /** + * Returns if running in quiet mode. + * @return {@code true} if running in quiet mode + */ + boolean isQuiet(); + } diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java index ec4be5b5b09..976473a4507 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java @@ -77,7 +77,7 @@ public class AetherGrapeEngine implements GrapeEngine { RepositorySystem repositorySystem, DefaultRepositorySystemSession repositorySystemSession, List remoteRepositories, - DependencyResolutionContext resolutionContext) { + DependencyResolutionContext resolutionContext, boolean quiet) { this.classLoader = classLoader; this.repositorySystem = repositorySystem; this.session = repositorySystemSession; @@ -88,12 +88,14 @@ public class AetherGrapeEngine implements GrapeEngine { for (RemoteRepository repository : remotes) { addRepository(repository); } - this.progressReporter = getProgressReporter(this.session); + this.progressReporter = getProgressReporter(this.session, quiet); } - private ProgressReporter getProgressReporter(DefaultRepositorySystemSession session) { - String progressReporter = System.getProperty( - "org.springframework.boot.cli.compiler.grape.ProgressReporter"); + private ProgressReporter getProgressReporter(DefaultRepositorySystemSession session, + boolean quiet) { + String progressReporter = (quiet ? "none" + : System.getProperty( + "org.springframework.boot.cli.compiler.grape.ProgressReporter")); if ("detail".equals(progressReporter) || Boolean.getBoolean("groovy.grape.report.downloads")) { return new DetailedProgressReporter(session, System.out); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java index 8878a67041e..946e5cd1d29 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineFactory.java @@ -44,27 +44,21 @@ public abstract class AetherGrapeEngineFactory { public static AetherGrapeEngine create(GroovyClassLoader classLoader, List repositoryConfigurations, - DependencyResolutionContext dependencyResolutionContext) { - + DependencyResolutionContext dependencyResolutionContext, boolean quiet) { RepositorySystem repositorySystem = createServiceLocator() .getService(RepositorySystem.class); - DefaultRepositorySystemSession repositorySystemSession = MavenRepositorySystemUtils .newSession(); - ServiceLoader autoConfigurations = ServiceLoader .load(RepositorySystemSessionAutoConfiguration.class); - for (RepositorySystemSessionAutoConfiguration autoConfiguration : autoConfigurations) { autoConfiguration.apply(repositorySystemSession, repositorySystem); } - new DefaultRepositorySystemSessionAutoConfiguration() .apply(repositorySystemSession, repositorySystem); - return new AetherGrapeEngine(classLoader, repositorySystem, repositorySystemSession, createRepositories(repositoryConfigurations), - dependencyResolutionContext); + dependencyResolutionContext, quiet); } private static ServiceLocator createServiceLocator() { diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/GroovyGrabDependencyResolverTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/GroovyGrabDependencyResolverTests.java index 613612c0c9a..79760c550ef 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/GroovyGrabDependencyResolverTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/install/GroovyGrabDependencyResolverTests.java @@ -81,6 +81,11 @@ public class GroovyGrabDependencyResolverTests { return new String[] { "." }; } + @Override + public boolean isQuiet() { + return false; + } + }; this.resolver = new GroovyGrabDependencyResolver(configuration); } diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java index 048b0d04aed..57764484755 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngineTests.java @@ -59,7 +59,7 @@ public class AetherGrapeEngineTests { dependencyResolutionContext.addDependencyManagement( new SpringBootDependenciesDependencyManagement()); return AetherGrapeEngineFactory.create(this.groovyClassLoader, - repositoryConfigurations, dependencyResolutionContext); + repositoryConfigurations, dependencyResolutionContext, false); } @Test diff --git a/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc b/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc index 45fc7ba4184..7d8ac458eb1 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc @@ -283,6 +283,8 @@ the `Main-Class` attribute and leave out `Start-Class`. * `loader.path` can contain directories (scanned recursively for jar and zip files), archive paths, a directory within an archive that is scanned for jar files (for example, `dependencies.jar!/lib`), or wildcard patterns (for the default JVM behavior). + Archive paths can be relative to `loader.home`, or anywhere in the file system with a + `jar:file:` prefix. * `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a nested one if running from an archive). Because of this `PropertiesLauncher` behaves the same as `JarLauncher` when no additional configuration is provided. diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index edea2fdbd88..96d70f16b7c 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -250,8 +250,7 @@ public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstr protected Class[] getOrFindConfigurationClasses( MergedContextConfiguration mergedConfig) { Class[] classes = mergedConfig.getClasses(); - if (containsNonTestComponent(classes) || mergedConfig.hasLocations() - || !mergedConfig.getContextInitializerClasses().isEmpty()) { + if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) { return classes; } Class found = new SpringBootConfigurationFinder() diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperWithInitializersTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperWithInitializersTests.java new file mode 100644 index 00000000000..c572dbbf199 --- /dev/null +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperWithInitializersTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2016 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.test.context.bootstrap; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.boot.test.context.bootstrap.SpringBootTestContextBootstrapperWithInitializersTests.CustomInitializer; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.BootstrapWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SpringBootTestContextBootstrapper} with and + * {@link ApplicationContextInitializer}. + * + * @author Phillip Webb + */ +@RunWith(SpringRunner.class) +@BootstrapWith(SpringBootTestContextBootstrapper.class) +@ContextConfiguration(initializers = CustomInitializer.class) +public class SpringBootTestContextBootstrapperWithInitializersTests { + + @Autowired + private ApplicationContext context; + + @Test + public void foundConfiguration() throws Exception { + Object bean = this.context + .getBean(SpringBootTestContextBootstrapperExampleConfig.class); + assertThat(bean).isNotNull(); + } + + // gh-8483 + + public static class CustomInitializer + implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + } + + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java index 677c2212d86..ee828072a07 100755 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -25,8 +25,10 @@ import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -299,11 +301,9 @@ public class PropertiesLauncher extends Launcher { List paths = new ArrayList<>(); for (String path : commaSeparatedPaths.split(",")) { path = cleanupPath(path); - // Empty path (i.e. the archive itself if running from a JAR) is always added - // to the classpath so no need for it to be explicitly listed - if (!path.equals("")) { - paths.add(path); - } + // "" means the user wants root of archive but not current directory + path = ("".equals(path) ? "/" : path); + paths.add(path); } if (paths.isEmpty()) { paths.add("lib"); @@ -336,7 +336,13 @@ public class PropertiesLauncher extends Launcher { @Override protected ClassLoader createClassLoader(List archives) throws Exception { - ClassLoader loader = super.createClassLoader(archives); + Set urls = new LinkedHashSet(archives.size()); + for (Archive archive : archives) { + urls.add(archive.getUrl()); + } + ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]), + getClass().getClassLoader()); + debug("Classpath: " + urls); String customLoaderClassName = getProperty("loader.classLoader"); if (customLoaderClassName != null) { loader = wrapWithCustomClassLoader(loader, customLoaderClassName); @@ -454,13 +460,15 @@ public class PropertiesLauncher extends Launcher { String root = cleanupPath(stripFileUrlPrefix(path)); List lib = new ArrayList<>(); File file = new File(root); - if (!isAbsolutePath(root)) { - file = new File(this.home, root); - } - if (file.isDirectory()) { - debug("Adding classpath entries from " + file); - Archive archive = new ExplodedArchive(file, false); - lib.add(archive); + if (!"/".equals(root)) { + if (!isAbsolutePath(root)) { + file = new File(this.home, root); + } + if (file.isDirectory()) { + debug("Adding classpath entries from " + file); + Archive archive = new ExplodedArchive(file, false); + lib.add(archive); + } } Archive archive = getArchive(file); if (archive != null) { @@ -488,24 +496,46 @@ public class PropertiesLauncher extends Launcher { return null; } - private List getNestedArchives(String root) throws Exception { - if (root.startsWith("/") - || this.parent.getUrl().equals(this.home.toURI().toURL())) { + private List getNestedArchives(String path) throws Exception { + Archive parent = this.parent; + String root = path; + if (!root.equals("/") && root.startsWith("/") + || parent.getUrl().equals(this.home.toURI().toURL())) { // If home dir is same as parent archive, no need to add it twice. return null; } - Archive parent = this.parent; - if (root.startsWith("jar:file:") && root.contains("!")) { + if (root.contains("!")) { int index = root.indexOf("!"); - String file = root.substring("jar:file:".length(), index); - parent = new JarFileArchive(new File(file)); + File file = new File(this.home, root.substring(0, index)); + if (root.startsWith("jar:file:")) { + file = new File(root.substring("jar:file:".length(), index)); + } + parent = new JarFileArchive(file); root = root.substring(index + 1, root.length()); while (root.startsWith("/")) { root = root.substring(1); } } + if (root.endsWith(".jar")) { + File file = new File(this.home, root); + if (file.exists()) { + parent = new JarFileArchive(file); + root = ""; + } + } + if (root.equals("/") || root.equals("./") || root.equals(".")) { + // The prefix for nested jars is actually empty if it's at the root + root = ""; + } EntryFilter filter = new PrefixMatchingArchiveFilter(root); - return parent.getNestedArchives(filter); + List archives = new ArrayList(parent.getNestedArchives(filter)); + if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar") + && parent != this.parent) { + // You can't find the root with an entry filter so it has to be added + // explicitly. But don't add the root of the parent archive. + archives.add(parent); + } + return archives; } private void addNestedEntries(List lib) { @@ -518,7 +548,7 @@ public class PropertiesLauncher extends Launcher { @Override public boolean matches(Entry entry) { if (entry.isDirectory()) { - return entry.getName().startsWith(JarLauncher.BOOT_INF_CLASSES); + return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES); } return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB); } @@ -607,6 +637,9 @@ public class PropertiesLauncher extends Launcher { @Override public boolean matches(Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals(this.prefix); + } return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java index 3610844a85a..98be63ec579 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java @@ -21,12 +21,13 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.jar.Attributes; import java.util.jar.Manifest; +import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -36,6 +37,9 @@ import org.junit.rules.TemporaryFolder; import org.mockito.MockitoAnnotations; import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.core.io.FileSystemResource; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -72,6 +76,7 @@ public class PropertiesLauncherTests { System.clearProperty("loader.config.name"); System.clearProperty("loader.config.location"); System.clearProperty("loader.system"); + System.clearProperty("loader.classLoader"); } @Test @@ -131,6 +136,16 @@ public class PropertiesLauncherTests { .isEqualTo("[.]"); } + @Test + public void testUserSpecifiedSlashPath() throws Exception { + System.setProperty("loader.path", "jars/"); + PropertiesLauncher launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) + .isEqualTo("[jars/]"); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, endingWith("app.jar!/")); + } + @Test public void testUserSpecifiedWildcardPath() throws Exception { System.setProperty("loader.path", "jars/*"); @@ -153,13 +168,44 @@ public class PropertiesLauncherTests { waitFor("Hello World"); } + @Test + public void testUserSpecifiedRootOfJarPath() throws Exception { + System.setProperty("loader.path", + "jar:file:./src/test/resources/nested-jars/app.jar!/"); + PropertiesLauncher launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) + .isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar!/")); + } + + @Test + public void testUserSpecifiedRootOfJarPathWithDot() throws Exception { + System.setProperty("loader.path", "nested-jars/app.jar!/./"); + PropertiesLauncher launcher = new PropertiesLauncher(); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar!/")); + } + + @Test + public void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { + System.setProperty("loader.path", + "jar:file:./src/test/resources/nested-jars/app.jar!/./"); + PropertiesLauncher launcher = new PropertiesLauncher(); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + } + @Test public void testUserSpecifiedJarFileWithNestedArchives() throws Exception { System.setProperty("loader.path", "nested-jars/app.jar"); System.setProperty("loader.main", "demo.Application"); PropertiesLauncher launcher = new PropertiesLauncher(); - launcher.launch(new String[0]); - waitFor("Hello World"); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar!/")); } @Test @@ -209,11 +255,28 @@ public class PropertiesLauncherTests { public void testCustomClassLoaderCreation() throws Exception { System.setProperty("loader.classLoader", TestLoader.class.getName()); PropertiesLauncher launcher = new PropertiesLauncher(); - ClassLoader loader = launcher.createClassLoader(Collections.emptyList()); + ClassLoader loader = launcher.createClassLoader(archives()); assertThat(loader).isNotNull(); assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName()); } + private List archives() throws Exception { + List archives = new ArrayList(); + String path = System.getProperty("java.class.path"); + for (String url : path.split(File.pathSeparator)) { + archives.add(archive(url)); + } + return archives; + } + + private Archive archive(String url) throws IOException { + File file = new FileSystemResource(url).getFile(); + if (url.endsWith(".jar")) { + return new JarFileArchive(file); + } + return new ExplodedArchive(file); + } + @Test public void testUserSpecifiedConfigPathWins() throws Exception { @@ -280,6 +343,17 @@ public class PropertiesLauncherTests { assertThat(timeout).as("Timed out waiting for (" + value + ")").isTrue(); } + private Condition endingWith(final String value) { + return new Condition() { + + @Override + public boolean matches(Archive archive) { + return archive.toString().endsWith(value); + } + + }; + } + public static class TestLoader extends URLClassLoader { public TestLoader(ClassLoader parent) { diff --git a/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar b/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar index a4365eec85b..5600ed279ef 100644 Binary files a/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar and b/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar differ