Skip to content

Commit 1d83e87

Browse files
committed
Validate @ConfigurationProperties on @bean methods
Refactor `ConfigurationPropertiesBindingPostProcessor` to allow JSR-303 validation on `@ConfigurationProperties` defined at the `@Bean` method level. JSR-303 validation is now applied when a JSR-303 implementation is available and `@Validated` is present on either the configuration properties class itself or the `@Bean` method that creates it. Standard Spring validation is also supported using a validator bean named `configurationPropertiesValidator`, or by having the configuration properties implement `Validator`. The commit also consolidates tests into a single location. Fixes gh-10803
1 parent 9e75680 commit 1d83e87

24 files changed

+2126
-2633
lines changed

spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,9 +1213,13 @@ to your fields, as shown in the following example:
12131213
}
12141214
----
12151215

1216-
In order to validate the values of nested properties, you must annotate the associated
1217-
field as `@Valid` to trigger its validation. The following example builds on the
1218-
preceding `AcmeProperties` example:
1216+
TIP: You can also trigger validation by annotating the `@Bean` method that creates the
1217+
configuration properties with `@Validated`.
1218+
1219+
Although nested properties will also be validated when bound, it's good practice to
1220+
also annotate the associated field as `@Valid`. This ensure that validation is triggered
1221+
even if no nested properties are found. The following example builds on the preceding
1222+
`AcmeProperties` example:
12191223

12201224
[source,java,indent=0]
12211225
----

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
*/
4040
public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {
4141

42+
/**
43+
* The bean name that this class is registered with.
44+
*/
45+
public static final String BEAN_NAME = ConfigurationBeanFactoryMetadata.class
46+
.getName();
47+
4248
private ConfigurableListableBeanFactory beanFactory;
4349

4450
private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2012-2018 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.context.properties;
18+
19+
import org.springframework.beans.factory.BeanCreationException;
20+
import org.springframework.util.ClassUtils;
21+
22+
/**
23+
* Exception thrown when {@link ConfigurationProperties @ConfigurationProperties} binding
24+
* fails.
25+
*
26+
* @author Phillip Webb
27+
* @author Stephane Nicoll
28+
* @since 2.0.0
29+
*/
30+
public class ConfigurationPropertiesBindException extends BeanCreationException {
31+
32+
private final Class<?> beanType;
33+
34+
private final ConfigurationProperties annotation;
35+
36+
ConfigurationPropertiesBindException(String beanName, Object bean,
37+
ConfigurationProperties annotation, Exception cause) {
38+
super(beanName, getMessage(bean, annotation), cause);
39+
this.beanType = bean.getClass();
40+
this.annotation = annotation;
41+
}
42+
43+
/**
44+
* Return the bean type that was being bound.
45+
* @return the bean type
46+
*/
47+
public Class<?> getBeanType() {
48+
return this.beanType;
49+
}
50+
51+
/**
52+
* Return the configuration properties annotation that triggered the binding.
53+
* @return the configuration properties annotation
54+
*/
55+
public ConfigurationProperties getAnnotation() {
56+
return this.annotation;
57+
}
58+
59+
private static String getMessage(Object bean, ConfigurationProperties annotation) {
60+
StringBuilder message = new StringBuilder();
61+
message.append("Could not bind properties to '"
62+
+ ClassUtils.getShortName(bean.getClass()) + "' : ");
63+
message.append("prefix=").append(annotation.prefix());
64+
message.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
65+
message.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields());
66+
return message.toString();
67+
}
68+
69+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java

Lines changed: 60 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -16,93 +16,88 @@
1616

1717
package org.springframework.boot.context.properties;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
1922
import org.springframework.boot.context.properties.bind.BindHandler;
2023
import org.springframework.boot.context.properties.bind.Bindable;
2124
import org.springframework.boot.context.properties.bind.Binder;
2225
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
2326
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
2427
import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler;
2528
import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler;
26-
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
2729
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
2830
import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter;
29-
import org.springframework.core.ResolvableType;
30-
import org.springframework.core.convert.ConversionService;
31-
import org.springframework.core.env.PropertySource;
31+
import org.springframework.context.ApplicationContext;
32+
import org.springframework.core.env.PropertySources;
3233
import org.springframework.util.Assert;
33-
import org.springframework.util.ClassUtils;
34-
import org.springframework.validation.Errors;
3534
import org.springframework.validation.Validator;
35+
import org.springframework.validation.annotation.Validated;
3636

3737
/**
38-
* Bind {@link ConfigurationProperties} annotated object from a configurable list of
39-
* {@link PropertySource}.
38+
* Internal class by the {@link ConfigurationPropertiesBindingPostProcessor} to handle the
39+
* actual {@link ConfigurationProperties} binding.
4040
*
4141
* @author Stephane Nicoll
42+
* @author Phillip Webb
4243
*/
4344
class ConfigurationPropertiesBinder {
4445

45-
private final Iterable<PropertySource<?>> propertySources;
46+
private final ApplicationContext applicationContext;
4647

47-
private final ConversionService conversionService;
48+
private final PropertySources propertySources;
4849

49-
private final Validator validator;
50+
private final Validator configurationPropertiesValidator;
5051

51-
private Iterable<ConfigurationPropertySource> configurationSources;
52+
private final Validator jsr303Validator;
5253

53-
private final Binder binder;
54+
private volatile Binder binder;
5455

55-
ConfigurationPropertiesBinder(Iterable<PropertySource<?>> propertySources,
56-
ConversionService conversionService, Validator validator) {
57-
Assert.notNull(propertySources, "PropertySources must not be null");
58-
this.propertySources = propertySources;
59-
this.conversionService = conversionService;
60-
this.validator = validator;
61-
this.configurationSources = ConfigurationPropertySources.from(propertySources);
62-
this.binder = new Binder(this.configurationSources,
63-
new PropertySourcesPlaceholdersResolver(this.propertySources),
64-
this.conversionService);
56+
ConfigurationPropertiesBinder(ApplicationContext applicationContext,
57+
String validatorBeanName) {
58+
this.applicationContext = applicationContext;
59+
this.propertySources = new PropertySourcesDeducer(applicationContext)
60+
.getPropertySources();
61+
this.configurationPropertiesValidator = getConfigurationPropertiesValidator(
62+
applicationContext, validatorBeanName);
63+
this.jsr303Validator = ConfigurationPropertiesJsr303Validator
64+
.getIfJsr303Present(applicationContext);
65+
}
6566

67+
public void bind(Bindable<?> target) {
68+
ConfigurationProperties annotation = target
69+
.getAnnotation(ConfigurationProperties.class);
70+
Assert.state(annotation != null, "Missing @ConfigurationProperties on " + target);
71+
List<Validator> validators = getValidators(target);
72+
BindHandler bindHandler = getBindHandler(annotation, validators);
73+
getBinder().bind(annotation.prefix(), target, bindHandler);
6674
}
6775

68-
/**
69-
* Bind the specified {@code target} object using the configuration defined by the
70-
* specified {@code annotation}.
71-
* @param target the target to bind the configuration property sources to
72-
* @param annotation the binding configuration
73-
* @param targetType the resolvable type for the target
74-
* @throws ConfigurationPropertiesBindingException if the binding failed
75-
*/
76-
void bind(Object target, ConfigurationProperties annotation,
77-
ResolvableType targetType) {
78-
Validator validator = determineValidator(target);
79-
BindHandler handler = getBindHandler(annotation, validator);
80-
Bindable<?> bindable = Bindable.of(targetType).withExistingValue(target);
81-
try {
82-
this.binder.bind(annotation.prefix(), bindable, handler);
83-
}
84-
catch (Exception ex) {
85-
String message = "Could not bind properties to '"
86-
+ ClassUtils.getShortName(target.getClass()) + "': "
87-
+ getAnnotationDetails(annotation);
88-
throw new ConfigurationPropertiesBindingException(message, ex);
76+
private Validator getConfigurationPropertiesValidator(
77+
ApplicationContext applicationContext, String validatorBeanName) {
78+
if (applicationContext.containsBean(validatorBeanName)) {
79+
return applicationContext.getBean(validatorBeanName, Validator.class);
8980
}
81+
return null;
9082
}
9183

92-
private Validator determineValidator(Object bean) {
93-
boolean supportsBean = (this.validator != null
94-
&& this.validator.supports(bean.getClass()));
95-
if (ClassUtils.isAssignable(Validator.class, bean.getClass())) {
96-
if (supportsBean) {
97-
return new ChainingValidator(this.validator, (Validator) bean);
98-
}
99-
return (Validator) bean;
84+
private List<Validator> getValidators(Bindable<?> target) {
85+
List<Validator> validators = new ArrayList<>(3);
86+
if (this.configurationPropertiesValidator != null) {
87+
validators.add(this.configurationPropertiesValidator);
88+
}
89+
if (this.jsr303Validator != null
90+
&& target.getAnnotation(Validated.class) != null) {
91+
validators.add(this.jsr303Validator);
10092
}
101-
return (supportsBean ? this.validator : null);
93+
if (target.getValue() != null && target.getValue().get() instanceof Validator) {
94+
validators.add((Validator) target.getValue().get());
95+
}
96+
return validators;
10297
}
10398

10499
private BindHandler getBindHandler(ConfigurationProperties annotation,
105-
Validator validator) {
100+
List<Validator> validators) {
106101
BindHandler handler = BindHandler.DEFAULT;
107102
if (annotation.ignoreInvalidFields()) {
108103
handler = new IgnoreErrorsBindHandler(handler);
@@ -111,55 +106,22 @@ private BindHandler getBindHandler(ConfigurationProperties annotation,
111106
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
112107
handler = new NoUnboundElementsBindHandler(handler, filter);
113108
}
114-
if (validator != null) {
115-
handler = new ValidationBindHandler(handler, validator);
109+
if (!validators.isEmpty()) {
110+
handler = new ValidationBindHandler(handler,
111+
validators.toArray(new Validator[validators.size()]));
116112
}
117113
return handler;
118114
}
119115

120-
private String getAnnotationDetails(ConfigurationProperties annotation) {
121-
if (annotation == null) {
122-
return "";
116+
private Binder getBinder() {
117+
if (this.binder == null) {
118+
this.binder = new Binder(
119+
ConfigurationPropertySources.from(this.propertySources),
120+
new PropertySourcesPlaceholdersResolver(this.propertySources),
121+
new ConversionServiceDeducer(this.applicationContext)
122+
.getConversionService());
123123
}
124-
StringBuilder details = new StringBuilder();
125-
details.append("prefix=").append(annotation.prefix());
126-
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
127-
details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields());
128-
return details.toString();
129-
}
130-
131-
/**
132-
* {@link Validator} implementation that wraps {@link Validator} instances and chains
133-
* their execution.
134-
*/
135-
private static class ChainingValidator implements Validator {
136-
137-
private final Validator[] validators;
138-
139-
ChainingValidator(Validator... validators) {
140-
Assert.notNull(validators, "Validators must not be null");
141-
this.validators = validators;
142-
}
143-
144-
@Override
145-
public boolean supports(Class<?> clazz) {
146-
for (Validator validator : this.validators) {
147-
if (validator.supports(clazz)) {
148-
return true;
149-
}
150-
}
151-
return false;
152-
}
153-
154-
@Override
155-
public void validate(Object target, Errors errors) {
156-
for (Validator validator : this.validators) {
157-
if (validator.supports(target.getClass())) {
158-
validator.validate(target, errors);
159-
}
160-
}
161-
}
162-
124+
return this.binder;
163125
}
164126

165127
}

0 commit comments

Comments
 (0)