Refine validator and MVC validator configuration

This commit ensures that a primary JSR 303 and Spring Validator will be
exposed if the auto-configuration kicks in. As `LocalValidatorFactoryBean`
exposes 3 contracts (JSR-303 `Validator` and `ValidatorFactory` as well as
the `Spring` validator one), this makes sure that those types can be
injected by type.

`LocalValidatorFactoryBean` exposes 3 contracts and we're only checking
for the absence of a `javax.validation.Validator` to auto-configure a
`LocalValidatorFactoryBean`. If no standard JSR validator exists but a
Spring's `Validator` exists and is primary, we shouldn't flag the
auto-configured one as `@Primary`. Previous iterations on this feature
have made sure that we'll auto-configure at most one
`javax.validation.Validator` so not flagging it `@Primary` is no problem.

This commit also restores and adds tests that validates
`ValidationAutoConfiguration` will configure a JSR validator even if a
Spring Validator is present.

This effectively fixes gh-8495 in a different way.

Closes gh-8979
Closes gh-8976
This commit is contained in:
Stephane Nicoll 2017-04-25 22:28:56 +02:00
parent f42998f5ca
commit 1de2316a0b
4 changed files with 339 additions and 34 deletions

View File

@ -0,0 +1,88 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.validation;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/**
* Enable the {@code Primary} flag on the auto-configured validator if necessary.
* <p>
* As {@link LocalValidatorFactoryBean} exposes 3 validator related contracts and we're
* only checking for the absence {@link javax.validation.Validator}, we should flag the
* auto-configured validator as primary only if no Spring's {@link Validator} is flagged
* as primary.
*
* @author Stephane Nicoll
*/
class PrimaryDefaultValidatorPostProcessor
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
/**
* The bean name of the auto-configured Validator.
*/
private static final String VALIDATOR_BEAN_NAME = "defaultValidator";
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
if (!registry.containsBeanDefinition(VALIDATOR_BEAN_NAME)) {
return;
}
BeanDefinition def = registry.getBeanDefinition(VALIDATOR_BEAN_NAME);
if (def != null
&& this.beanFactory.isTypeMatch(VALIDATOR_BEAN_NAME, LocalValidatorFactoryBean.class)
&& def.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
def.setPrimary(!hasPrimarySpringValidator(registry));
}
}
private boolean hasPrimarySpringValidator(BeanDefinitionRegistry registry) {
String[] validatorBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Validator.class, false, false);
for (String validatorBean : validatorBeans) {
BeanDefinition def = registry.getBeanDefinition(validatorBean);
if (def != null && def.isPrimary()) {
return true;
}
}
return false;
}
}

View File

@ -28,6 +28,7 @@ import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@ -43,12 +44,13 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess
@Configuration
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean
public static Validator jsr303Validator() {
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());

View File

@ -31,15 +31,20 @@ import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ValidationAutoConfiguration}.
*
* @author Stephane Nicoll
* @author Phillip Webb
*/
public class ValidationAutoConfigurationTests {
@ -55,6 +60,95 @@ public class ValidationAutoConfigurationTests {
}
}
@Test
public void validationAutoConfigurationShouldConfigureDefaultValidator() {
load(Config.class);
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
String[] springValidatorNames = this.context
.getBeanNamesForType(org.springframework.validation.Validator.class);
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
assertThat(springValidatorNames).containsExactly("defaultValidator");
Validator jsrValidator = this.context.getBean(Validator.class);
org.springframework.validation.Validator springValidator = this.context
.getBean(org.springframework.validation.Validator.class);
assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class);
assertThat(jsrValidator).isEqualTo(springValidator);
assertThat(isPrimaryBean("defaultValidator")).isTrue();
}
@Test
public void validationAutoConfigurationWhenUserProvidesValidatorShouldBackOff() {
load(UserDefinedValidatorConfig.class);
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
String[] springValidatorNames = this.context
.getBeanNamesForType(org.springframework.validation.Validator.class);
assertThat(jsrValidatorNames).containsExactly("customValidator");
assertThat(springValidatorNames).containsExactly("customValidator");
org.springframework.validation.Validator springValidator = this.context
.getBean(org.springframework.validation.Validator.class);
Validator jsrValidator = this.context.getBean(Validator.class);
assertThat(jsrValidator).isInstanceOf(OptionalValidatorFactoryBean.class);
assertThat(jsrValidator).isEqualTo(springValidator);
assertThat(isPrimaryBean("customValidator")).isFalse();
}
@Test
public void validationAutoConfigurationWhenUserProvidesDefaultValidatorShouldNotEnablePrimary() {
load(UserDefinedDefaultValidatorConfig.class);
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
String[] springValidatorNames = this.context
.getBeanNamesForType(org.springframework.validation.Validator.class);
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
assertThat(springValidatorNames).containsExactly("defaultValidator");
assertThat(isPrimaryBean("defaultValidator")).isFalse();
}
@Test
public void validationAutoConfigurationWhenUserProvidesJsrValidatorShouldBackOff() {
load(UserDefinedJsrValidatorConfig.class);
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
String[] springValidatorNames = this.context
.getBeanNamesForType(org.springframework.validation.Validator.class);
assertThat(jsrValidatorNames).containsExactly("customValidator");
assertThat(springValidatorNames).isEmpty();
assertThat(isPrimaryBean("customValidator")).isFalse();
}
@Test
public void validationAutoConfigurationWhenUserProvidesSpringValidatorShouldCreateJsrValidator() {
load(UserDefinedSpringValidatorConfig.class);
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
String[] springValidatorNames = this.context
.getBeanNamesForType(org.springframework.validation.Validator.class);
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
assertThat(springValidatorNames).containsExactly(
"customValidator", "anotherCustomValidator", "defaultValidator");
Validator jsrValidator = this.context.getBean(Validator.class);
org.springframework.validation.Validator springValidator = this.context
.getBean(org.springframework.validation.Validator.class);
assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class);
assertThat(jsrValidator).isEqualTo(springValidator);
assertThat(isPrimaryBean("defaultValidator")).isTrue();
}
@Test
public void validationAutoConfigurationWhenUserProvidesPrimarySpringValidatorShouldRemovePrimaryFlag() {
load(UserDefinedPrimarySpringValidatorConfig.class);
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
String[] springValidatorNames = this.context
.getBeanNamesForType(org.springframework.validation.Validator.class);
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
assertThat(springValidatorNames).containsExactly(
"customValidator", "anotherCustomValidator", "defaultValidator");
Validator jsrValidator = this.context.getBean(Validator.class);
org.springframework.validation.Validator springValidator = this.context
.getBean(org.springframework.validation.Validator.class);
assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class);
assertThat(springValidator).isEqualTo(
this.context.getBean("anotherCustomValidator"));
assertThat(isPrimaryBean("defaultValidator")).isFalse();
}
@Test
public void validationIsEnabled() {
load(SampleService.class);
@ -104,7 +198,11 @@ public class ValidationAutoConfigurationTests {
.getPropertyValue("validator"));
}
public void load(Class<?> config, String... environment) {
private boolean isPrimaryBean(String beanName) {
return this.context.getBeanDefinition(beanName).isPrimary();
}
private void load(Class<?> config, String... environment) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
EnvironmentTestUtils.addEnvironment(ctx, environment);
if (config != null) {
@ -115,6 +213,72 @@ public class ValidationAutoConfigurationTests {
this.context = ctx;
}
@Configuration
static class Config {
}
@Configuration
static class UserDefinedValidatorConfig {
@Bean
public OptionalValidatorFactoryBean customValidator() {
return new OptionalValidatorFactoryBean();
}
}
@Configuration
static class UserDefinedDefaultValidatorConfig {
@Bean
public OptionalValidatorFactoryBean defaultValidator() {
return new OptionalValidatorFactoryBean();
}
}
@Configuration
static class UserDefinedJsrValidatorConfig {
@Bean
public Validator customValidator() {
return mock(Validator.class);
}
}
@Configuration
static class UserDefinedSpringValidatorConfig {
@Bean
public org.springframework.validation.Validator customValidator() {
return mock(org.springframework.validation.Validator.class);
}
@Bean
public org.springframework.validation.Validator anotherCustomValidator() {
return mock(org.springframework.validation.Validator.class);
}
}
@Configuration
static class UserDefinedPrimarySpringValidatorConfig {
@Bean
public org.springframework.validation.Validator customValidator() {
return mock(org.springframework.validation.Validator.class);
}
@Bean
@Primary
public org.springframework.validation.Validator anotherCustomValidator() {
return mock(org.springframework.validation.Validator.class);
}
}
@Validated
static class SampleService {

View File

@ -39,6 +39,7 @@ import org.junit.rules.ExpectedException;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WelcomePageHandlerMapping;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
@ -59,6 +60,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
@ -655,77 +657,126 @@ public class WebMvcAutoConfigurationTests {
}
@Test
public void validationNoJsr303ValidatorExposedByDefault() {
load();
public void validatorWhenNoValidatorShouldUseDefault() {
load(null, new Class<?>[] { ValidationAutoConfiguration.class });
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.isEmpty();
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("mvcValidator");
}
@Test
public void validationCustomConfigurerTakesPrecedence() {
load(MvcValidator.class);
public void validatorWhenNoCustomizationShouldUseAutoConfigured() {
load();
String[] jsrValidatorBeans = this.context
.getBeanNamesForType(javax.validation.Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(jsrValidatorBeans).containsExactly("defaultValidator");
assertThat(springValidatorBeans).containsExactly("defaultValidator", "mvcValidator");
Validator validator = this.context.getBean("mvcValidator", Validator.class);
assertThat(validator).isInstanceOf(WebMvcValidator.class);
Object defaultValidator = this.context.getBean("defaultValidator");
assertThat(((WebMvcValidator) validator).getTarget()).isSameAs(defaultValidator);
// Primary Spring validator is the one use by MVC behind the scenes
assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator);
}
@Test
public void validatorWithConfigurerShouldUseSpringValidator() {
load(MvcValidator.class, new Class<?>[] { ValidationAutoConfiguration.class });
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.isEmpty();
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1);
Validator validator = this.context.getBean(Validator.class);
assertThat(validator)
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("mvcValidator");
assertThat(this.context.getBean("mvcValidator"))
.isSameAs(this.context.getBean(MvcValidator.class).validator);
}
@Test
public void validationCustomConfigurerTakesPrecedenceAndDoNotExposeJsr303() {
load(MvcJsr303Validator.class);
public void validatorWithConfigurerDoesNotExposeJsr303() {
load(MvcJsr303Validator.class, new Class<?>[] { ValidationAutoConfiguration.class });
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.isEmpty();
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1);
Validator validator = this.context.getBean(Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("mvcValidator");
Validator validator = this.context.getBean("mvcValidator", Validator.class);
assertThat(validator).isInstanceOf(WebMvcValidator.class);
assertThat(((WebMvcValidator) validator).getTarget())
.isSameAs(this.context.getBean(MvcJsr303Validator.class).validator);
}
@Test
public void validationJsr303CustomValidatorReusedAsSpringValidator() {
load(CustomValidator.class);
public void validatorWithConfigurerTakesPrecedence() {
load(MvcValidator.class);
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).hasSize(1);
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.hasSize(1);
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(2);
Validator validator = this.context.getBean("mvcValidator", Validator.class);
assertThat(validator).isInstanceOf(WebMvcValidator.class);
assertThat(((WebMvcValidator) validator).getTarget())
.isSameAs(this.context.getBean(javax.validation.Validator.class));
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("defaultValidator", "mvcValidator");
assertThat(this.context.getBean("mvcValidator"))
.isSameAs(this.context.getBean(MvcValidator.class).validator);
// Primary Spring validator is the auto-configured one as the MVC one has been
// customized via a WebMvcConfigurer
assertThat(this.context.getBean(Validator.class))
.isEqualTo(this.context.getBean("defaultValidator"));
}
@Test
public void validationJsr303ValidatorExposedAsSpringValidator() {
load(Jsr303Validator.class);
public void validatorWithCustomSpringValidatorIgnored() {
load(CustomSpringValidator.class);
String[] jsrValidatorBeans = this.context
.getBeanNamesForType(javax.validation.Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(jsrValidatorBeans).containsExactly("defaultValidator");
assertThat(springValidatorBeans).containsExactly(
"customSpringValidator", "defaultValidator", "mvcValidator");
Validator validator = this.context.getBean("mvcValidator", Validator.class);
assertThat(validator).isInstanceOf(WebMvcValidator.class);
Object defaultValidator = this.context.getBean("defaultValidator");
assertThat(((WebMvcValidator) validator).getTarget())
.isSameAs(defaultValidator);
// Primary Spring validator is the one use by MVC behind the scenes
assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator);
}
@Test
public void validatorWithCustomJsr303ValidatorExposedAsSpringValidator() {
load(CustomJsr303Validator.class);
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.hasSize(1);
assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1);
String[] jsrValidatorBeans = this.context
.getBeanNamesForType(javax.validation.Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(jsrValidatorBeans).containsExactly("customJsr303Validator");
assertThat(springValidatorBeans).containsExactly("mvcValidator");
Validator validator = this.context.getBean(Validator.class);
assertThat(validator).isInstanceOf(WebMvcValidator.class);
SpringValidatorAdapter target = ((WebMvcValidator) validator)
.getTarget();
assertThat(new DirectFieldAccessor(target).getPropertyValue("targetValidator"))
.isSameAs(this.context.getBean(javax.validation.Validator.class));
.isSameAs(this.context.getBean("customJsr303Validator"));
}
private void load(Class<?> config, String... environment) {
load(config, null, environment);
}
private void load(Class<?> config, Class<?>[] exclude, String... environment) {
this.context = new AnnotationConfigEmbeddedWebApplicationContext();
EnvironmentTestUtils.addEnvironment(this.context, environment);
List<Class<?>> configClasses = new ArrayList<Class<?>>();
if (config != null) {
configClasses.add(config);
}
configClasses.addAll(Arrays.asList(Config.class, WebMvcAutoConfiguration.class,
configClasses.addAll(Arrays.asList(Config.class,
ValidationAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class));
if (!ObjectUtils.isEmpty(exclude)) {
configClasses.removeAll(Arrays.asList(exclude));
}
this.context.register(configClasses.toArray(new Class<?>[configClasses.size()]));
this.context.refresh();
}
@ -919,21 +970,21 @@ public class WebMvcAutoConfigurationTests {
}
@Configuration
static class Jsr303Validator {
static class CustomJsr303Validator {
@Bean
public javax.validation.Validator jsr303Validator() {
public javax.validation.Validator customJsr303Validator() {
return mock(javax.validation.Validator.class);
}
}
@Configuration
static class CustomValidator {
static class CustomSpringValidator {
@Bean
public Validator customValidator() {
return new LocalValidatorFactoryBean();
public Validator customSpringValidator() {
return mock(Validator.class);
}
}