Skip to content

Commit 1de2316

Browse files
committed
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
1 parent f42998f commit 1de2316

File tree

4 files changed

+336
-31
lines changed

4 files changed

+336
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.validation;
18+
19+
import org.springframework.beans.BeansException;
20+
import org.springframework.beans.factory.BeanFactory;
21+
import org.springframework.beans.factory.BeanFactoryAware;
22+
import org.springframework.beans.factory.BeanFactoryUtils;
23+
import org.springframework.beans.factory.config.BeanDefinition;
24+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
25+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
26+
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
27+
import org.springframework.core.type.AnnotationMetadata;
28+
import org.springframework.validation.Validator;
29+
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
30+
31+
/**
32+
* Enable the {@code Primary} flag on the auto-configured validator if necessary.
33+
* <p>
34+
* As {@link LocalValidatorFactoryBean} exposes 3 validator related contracts and we're
35+
* only checking for the absence {@link javax.validation.Validator}, we should flag the
36+
* auto-configured validator as primary only if no Spring's {@link Validator} is flagged
37+
* as primary.
38+
*
39+
* @author Stephane Nicoll
40+
*/
41+
class PrimaryDefaultValidatorPostProcessor
42+
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
43+
44+
/**
45+
* The bean name of the auto-configured Validator.
46+
*/
47+
private static final String VALIDATOR_BEAN_NAME = "defaultValidator";
48+
49+
private ConfigurableListableBeanFactory beanFactory;
50+
51+
@Override
52+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
53+
if (beanFactory instanceof ConfigurableListableBeanFactory) {
54+
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
55+
}
56+
}
57+
58+
@Override
59+
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
60+
BeanDefinitionRegistry registry) {
61+
if (this.beanFactory == null) {
62+
return;
63+
}
64+
if (!registry.containsBeanDefinition(VALIDATOR_BEAN_NAME)) {
65+
return;
66+
}
67+
BeanDefinition def = registry.getBeanDefinition(VALIDATOR_BEAN_NAME);
68+
if (def != null
69+
&& this.beanFactory.isTypeMatch(VALIDATOR_BEAN_NAME, LocalValidatorFactoryBean.class)
70+
&& def.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
71+
def.setPrimary(!hasPrimarySpringValidator(registry));
72+
}
73+
}
74+
75+
private boolean hasPrimarySpringValidator(BeanDefinitionRegistry registry) {
76+
String[] validatorBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
77+
this.beanFactory, Validator.class, false, false);
78+
for (String validatorBean : validatorBeans) {
79+
BeanDefinition def = registry.getBeanDefinition(validatorBean);
80+
if (def != null && def.isPrimary()) {
81+
return true;
82+
}
83+
}
84+
return false;
85+
}
86+
87+
}
88+

spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.validation.MessageInterpolatorFactory;
2929
import org.springframework.context.annotation.Bean;
3030
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.context.annotation.Import;
3132
import org.springframework.context.annotation.Role;
3233
import org.springframework.core.env.Environment;
3334
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@@ -43,12 +44,13 @@
4344
@Configuration
4445
@ConditionalOnClass(ExecutableValidator.class)
4546
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
47+
@Import(PrimaryDefaultValidatorPostProcessor.class)
4648
public class ValidationAutoConfiguration {
4749

4850
@Bean
4951
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
50-
@ConditionalOnMissingBean
51-
public static Validator jsr303Validator() {
52+
@ConditionalOnMissingBean(Validator.class)
53+
public static LocalValidatorFactoryBean defaultValidator() {
5254
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
5355
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
5456
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());

spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,20 @@
3131
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3232
import org.springframework.context.annotation.Bean;
3333
import org.springframework.context.annotation.Configuration;
34+
import org.springframework.context.annotation.Primary;
3435
import org.springframework.validation.annotation.Validated;
36+
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
3537
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
38+
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
3639

3740
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.mockito.Mockito.mock;
3842

3943
/**
4044
* Tests for {@link ValidationAutoConfiguration}.
4145
*
4246
* @author Stephane Nicoll
47+
* @author Phillip Webb
4348
*/
4449
public class ValidationAutoConfigurationTests {
4550

@@ -55,6 +60,95 @@ public void close() {
5560
}
5661
}
5762

63+
@Test
64+
public void validationAutoConfigurationShouldConfigureDefaultValidator() {
65+
load(Config.class);
66+
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
67+
String[] springValidatorNames = this.context
68+
.getBeanNamesForType(org.springframework.validation.Validator.class);
69+
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
70+
assertThat(springValidatorNames).containsExactly("defaultValidator");
71+
Validator jsrValidator = this.context.getBean(Validator.class);
72+
org.springframework.validation.Validator springValidator = this.context
73+
.getBean(org.springframework.validation.Validator.class);
74+
assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class);
75+
assertThat(jsrValidator).isEqualTo(springValidator);
76+
assertThat(isPrimaryBean("defaultValidator")).isTrue();
77+
}
78+
79+
@Test
80+
public void validationAutoConfigurationWhenUserProvidesValidatorShouldBackOff() {
81+
load(UserDefinedValidatorConfig.class);
82+
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
83+
String[] springValidatorNames = this.context
84+
.getBeanNamesForType(org.springframework.validation.Validator.class);
85+
assertThat(jsrValidatorNames).containsExactly("customValidator");
86+
assertThat(springValidatorNames).containsExactly("customValidator");
87+
org.springframework.validation.Validator springValidator = this.context
88+
.getBean(org.springframework.validation.Validator.class);
89+
Validator jsrValidator = this.context.getBean(Validator.class);
90+
assertThat(jsrValidator).isInstanceOf(OptionalValidatorFactoryBean.class);
91+
assertThat(jsrValidator).isEqualTo(springValidator);
92+
assertThat(isPrimaryBean("customValidator")).isFalse();
93+
}
94+
95+
@Test
96+
public void validationAutoConfigurationWhenUserProvidesDefaultValidatorShouldNotEnablePrimary() {
97+
load(UserDefinedDefaultValidatorConfig.class);
98+
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
99+
String[] springValidatorNames = this.context
100+
.getBeanNamesForType(org.springframework.validation.Validator.class);
101+
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
102+
assertThat(springValidatorNames).containsExactly("defaultValidator");
103+
assertThat(isPrimaryBean("defaultValidator")).isFalse();
104+
}
105+
106+
@Test
107+
public void validationAutoConfigurationWhenUserProvidesJsrValidatorShouldBackOff() {
108+
load(UserDefinedJsrValidatorConfig.class);
109+
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
110+
String[] springValidatorNames = this.context
111+
.getBeanNamesForType(org.springframework.validation.Validator.class);
112+
assertThat(jsrValidatorNames).containsExactly("customValidator");
113+
assertThat(springValidatorNames).isEmpty();
114+
assertThat(isPrimaryBean("customValidator")).isFalse();
115+
}
116+
117+
@Test
118+
public void validationAutoConfigurationWhenUserProvidesSpringValidatorShouldCreateJsrValidator() {
119+
load(UserDefinedSpringValidatorConfig.class);
120+
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
121+
String[] springValidatorNames = this.context
122+
.getBeanNamesForType(org.springframework.validation.Validator.class);
123+
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
124+
assertThat(springValidatorNames).containsExactly(
125+
"customValidator", "anotherCustomValidator", "defaultValidator");
126+
Validator jsrValidator = this.context.getBean(Validator.class);
127+
org.springframework.validation.Validator springValidator = this.context
128+
.getBean(org.springframework.validation.Validator.class);
129+
assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class);
130+
assertThat(jsrValidator).isEqualTo(springValidator);
131+
assertThat(isPrimaryBean("defaultValidator")).isTrue();
132+
}
133+
134+
@Test
135+
public void validationAutoConfigurationWhenUserProvidesPrimarySpringValidatorShouldRemovePrimaryFlag() {
136+
load(UserDefinedPrimarySpringValidatorConfig.class);
137+
String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class);
138+
String[] springValidatorNames = this.context
139+
.getBeanNamesForType(org.springframework.validation.Validator.class);
140+
assertThat(jsrValidatorNames).containsExactly("defaultValidator");
141+
assertThat(springValidatorNames).containsExactly(
142+
"customValidator", "anotherCustomValidator", "defaultValidator");
143+
Validator jsrValidator = this.context.getBean(Validator.class);
144+
org.springframework.validation.Validator springValidator = this.context
145+
.getBean(org.springframework.validation.Validator.class);
146+
assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class);
147+
assertThat(springValidator).isEqualTo(
148+
this.context.getBean("anotherCustomValidator"));
149+
assertThat(isPrimaryBean("defaultValidator")).isFalse();
150+
}
151+
58152
@Test
59153
public void validationIsEnabled() {
60154
load(SampleService.class);
@@ -104,7 +198,11 @@ public void userDefinedMethodValidationPostProcessorTakesPrecedence() {
104198
.getPropertyValue("validator"));
105199
}
106200

107-
public void load(Class<?> config, String... environment) {
201+
private boolean isPrimaryBean(String beanName) {
202+
return this.context.getBeanDefinition(beanName).isPrimary();
203+
}
204+
205+
private void load(Class<?> config, String... environment) {
108206
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
109207
EnvironmentTestUtils.addEnvironment(ctx, environment);
110208
if (config != null) {
@@ -115,6 +213,72 @@ public void load(Class<?> config, String... environment) {
115213
this.context = ctx;
116214
}
117215

216+
@Configuration
217+
static class Config {
218+
219+
}
220+
221+
@Configuration
222+
static class UserDefinedValidatorConfig {
223+
224+
@Bean
225+
public OptionalValidatorFactoryBean customValidator() {
226+
return new OptionalValidatorFactoryBean();
227+
}
228+
229+
}
230+
231+
@Configuration
232+
static class UserDefinedDefaultValidatorConfig {
233+
234+
@Bean
235+
public OptionalValidatorFactoryBean defaultValidator() {
236+
return new OptionalValidatorFactoryBean();
237+
}
238+
239+
}
240+
241+
@Configuration
242+
static class UserDefinedJsrValidatorConfig {
243+
244+
@Bean
245+
public Validator customValidator() {
246+
return mock(Validator.class);
247+
}
248+
249+
}
250+
251+
@Configuration
252+
static class UserDefinedSpringValidatorConfig {
253+
254+
@Bean
255+
public org.springframework.validation.Validator customValidator() {
256+
return mock(org.springframework.validation.Validator.class);
257+
}
258+
259+
@Bean
260+
public org.springframework.validation.Validator anotherCustomValidator() {
261+
return mock(org.springframework.validation.Validator.class);
262+
}
263+
264+
}
265+
266+
@Configuration
267+
static class UserDefinedPrimarySpringValidatorConfig {
268+
269+
@Bean
270+
public org.springframework.validation.Validator customValidator() {
271+
return mock(org.springframework.validation.Validator.class);
272+
}
273+
274+
@Bean
275+
@Primary
276+
public org.springframework.validation.Validator anotherCustomValidator() {
277+
return mock(org.springframework.validation.Validator.class);
278+
}
279+
280+
}
281+
118282
@Validated
119283
static class SampleService {
120284

0 commit comments

Comments
 (0)