Skip to content

Commit 10dbf3c

Browse files
committed
Use @Validated as trigger for JSR-330 validation
Update `ConfigurationPropertiesBindingPostProcessor` so that `@Validated` is expected to be used to trigger JSR-330 validation. Any existing configuration classes that use JSR-330 annotations but don't have `@Validated` will currently still be validated, but will now log a warning. This should give users a chance to add the requested annotations before the next Spring Boot release where we will use them as the exclusive signal that validation is required. Closes gh-7579
1 parent f42ebe4 commit 10dbf3c

File tree

12 files changed

+142
-24
lines changed

12 files changed

+142
-24
lines changed

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

+7-3
Original file line numberDiff line numberDiff line change
@@ -1090,13 +1090,16 @@ only rely on custom converters qualified with `@ConfigurationPropertiesBinding`.
10901090

10911091
[[boot-features-external-config-validation]]
10921092
==== @ConfigurationProperties Validation
1093-
Spring Boot will attempt to validate external configuration, by default using JSR-303
1094-
(if it is on the classpath). You can simply add JSR-303 `javax.validation` constraint
1095-
annotations to your `@ConfigurationProperties` class:
1093+
Spring Boot will attempt to validate `@ConfigurationProperties` classes whenever they
1094+
annotated with Spring's `@Validated` annotation. You can use JSR-303 `javax.validation`
1095+
constraint annotations directly on your configuration class. Simply ensure that a
1096+
compliant JSR-303 implementation is on your classpath, then add constraint annotations to
1097+
your fields:
10961098

10971099
[source,java,indent=0]
10981100
----
10991101
@ConfigurationProperties(prefix="foo")
1102+
@Validated
11001103
public class FooProperties {
11011104
11021105
@NotNull
@@ -1114,6 +1117,7 @@ as `@Valid` to trigger its validation. For example, building upon the above
11141117
[source,java,indent=0]
11151118
----
11161119
@ConfigurationProperties(prefix="connection")
1120+
@Validated
11171121
public class FooProperties {
11181122
11191123
@NotNull

spring-boot-samples/spring-boot-sample-simple/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
<groupId>org.springframework.boot</groupId>
2525
<artifactId>spring-boot-starter</artifactId>
2626
</dependency>
27+
<dependency>
28+
<groupId>org.hibernate</groupId>
29+
<artifactId>hibernate-validator</artifactId>
30+
</dependency>
2731
<!-- Test -->
2832
<dependency>
2933
<groupId>org.springframework.boot</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 sample.simple;
18+
19+
import javax.validation.constraints.NotNull;
20+
21+
import org.springframework.boot.context.properties.ConfigurationProperties;
22+
import org.springframework.stereotype.Component;
23+
24+
@Component
25+
@ConfigurationProperties(prefix = "sample")
26+
public class SampleConfigurationProperties {
27+
28+
@NotNull
29+
private String name;
30+
31+
public String getName() {
32+
return this.name;
33+
}
34+
35+
public void setName(String name) {
36+
this.name = name;
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
name: Phil
1+
name=Phil
2+
sample.name=Andy
3+

spring-boot-samples/spring-boot-sample-simple/src/test/java/sample/simple/SampleSimpleApplicationTests.java

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public void testDefaultSettings() throws Exception {
5858
SampleSimpleApplication.main(new String[0]);
5959
String output = this.outputCapture.toString();
6060
assertThat(output).contains("Hello Phil");
61+
assertThat(output).contains("The @ConfigurationProperties bean class "
62+
+ "sample.simple.SampleConfigurationProperties contains "
63+
+ "validation constraints but had not been annotated "
64+
+ "with @Validated");
6165
}
6266

6367
@Test

spring-boot-samples/spring-boot-sample-simple/src/test/resources/application.properties

-1
This file was deleted.

spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,9 @@ private void doBindPropertiesToTarget() throws BindException {
267267
relaxedTargetNames);
268268
dataBinder.bind(propertyValues);
269269
if (this.validator != null) {
270-
validate(dataBinder);
270+
dataBinder.validate();
271271
}
272+
checkForBindingErrors(dataBinder);
272273
}
273274

274275
private Iterable<String> getRelaxedTargetNames() {
@@ -338,8 +339,8 @@ private boolean isMapTarget() {
338339
return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
339340
}
340341

341-
private void validate(RelaxedDataBinder dataBinder) throws BindException {
342-
dataBinder.validate();
342+
private void checkForBindingErrors(RelaxedDataBinder dataBinder)
343+
throws BindException {
343344
BindingResult errors = dataBinder.getBindingResult();
344345
if (errors.hasErrors()) {
345346
logger.error("Properties configuration failed validation");

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.annotation.Target;
2424

2525
import org.springframework.core.annotation.AliasFor;
26+
import org.springframework.validation.annotation.Validated;
2627

2728
/**
2829
* Annotation for externalized configuration. Add this to a class definition or a
@@ -80,9 +81,10 @@
8081
boolean ignoreUnknownFields() default true;
8182

8283
/**
83-
* Flag to indicate that an exception should be raised if a Validator is available and
84-
* validation fails. If it is set to false, validation errors will be swallowed. They
85-
* will be logged, but not propagated to the caller.
84+
* Flag to indicate that an exception should be raised if a Validator is available,
85+
* the class is annotated with {@link Validated @Validated} and validation fails. If
86+
* it is set to false, validation errors will be swallowed. They will be logged, but
87+
* not propagated to the caller.
8688
* @return the flag value (default true)
8789
*/
8890
boolean exceptionIfInvalid() default true;

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

+35-13
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
4646
import org.springframework.core.Ordered;
4747
import org.springframework.core.PriorityOrdered;
48+
import org.springframework.core.annotation.AnnotatedElementUtils;
4849
import org.springframework.core.annotation.AnnotationUtils;
4950
import org.springframework.core.convert.ConversionService;
5051
import org.springframework.core.convert.converter.Converter;
@@ -61,6 +62,7 @@
6162
import org.springframework.util.StringUtils;
6263
import org.springframework.validation.Errors;
6364
import org.springframework.validation.Validator;
65+
import org.springframework.validation.annotation.Validated;
6466
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
6567

6668
/**
@@ -362,8 +364,8 @@ private Validator getValidator() {
362364
return this.validator;
363365
}
364366
if (this.localValidator == null && isJsr303Present()) {
365-
this.localValidator = new LocalValidatorFactory()
366-
.run(this.applicationContext);
367+
this.localValidator = new ValidatedLocalValidatorFactoryBean(
368+
this.applicationContext);
367369
}
368370
return this.localValidator;
369371
}
@@ -394,18 +396,38 @@ private ConversionService getDefaultConversionService() {
394396
}
395397

396398
/**
397-
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent class
398-
* loader issues.
399+
* {@link LocalValidatorFactoryBean} supports classes annotated with
400+
* {@link Validated @Validated}.
399401
*/
400-
private static class LocalValidatorFactory {
401-
402-
public Validator run(ApplicationContext applicationContext) {
403-
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
404-
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
405-
validator.setApplicationContext(applicationContext);
406-
validator.setMessageInterpolator(interpolatorFactory.getObject());
407-
validator.afterPropertiesSet();
408-
return validator;
402+
private static class ValidatedLocalValidatorFactoryBean
403+
extends LocalValidatorFactoryBean {
404+
405+
private static final Log logger = LogFactory
406+
.getLog(ConfigurationPropertiesBindingPostProcessor.class);
407+
408+
ValidatedLocalValidatorFactoryBean(ApplicationContext applicationContext) {
409+
setApplicationContext(applicationContext);
410+
setMessageInterpolator(new MessageInterpolatorFactory().getObject());
411+
afterPropertiesSet();
412+
}
413+
414+
@Override
415+
public boolean supports(Class<?> type) {
416+
if (!super.supports(type)) {
417+
return false;
418+
}
419+
if (AnnotatedElementUtils.isAnnotated(type, Validated.class)) {
420+
return true;
421+
}
422+
if (type.getPackage().getName().startsWith("org.springframework.boot")) {
423+
return false;
424+
}
425+
if (getConstraintsForClass(type).isBeanConstrained()) {
426+
logger.warn("The @ConfigurationProperties bean " + type
427+
+ " contains validation constraints but had not been annotated "
428+
+ "with @Validated.");
429+
}
430+
return true;
409431
}
410432

411433
}

spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.validation.Errors;
4747
import org.springframework.validation.ValidationUtils;
4848
import org.springframework.validation.Validator;
49+
import org.springframework.validation.annotation.Validated;
4950

5051
import static org.assertj.core.api.Assertions.assertThat;
5152
import static org.junit.Assert.fail;
@@ -456,6 +457,7 @@ public String getBar() {
456457
}
457458

458459
@ConfigurationProperties(prefix = "test")
460+
@Validated
459461
public static class PropertyWithJSR303 extends PropertyWithoutJSR303 {
460462

461463
@NotNull

spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java

+37
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.stereotype.Component;
3939
import org.springframework.test.context.support.TestPropertySourceUtils;
4040
import org.springframework.validation.BindException;
41+
import org.springframework.validation.annotation.Validated;
4142

4243
import static org.assertj.core.api.Assertions.assertThat;
4344

@@ -172,6 +173,17 @@ public void testExceptionOnValidation() {
172173
this.context.refresh();
173174
}
174175

176+
@Test
177+
public void testNoExceptionOnValidationWithoutValidated() {
178+
this.context.register(IgnoredIfInvalidButNotValidatedTestConfiguration.class);
179+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
180+
"name:foo");
181+
this.context.refresh();
182+
IgnoredIfInvalidButNotValidatedTestProperties bean = this.context
183+
.getBean(IgnoredIfInvalidButNotValidatedTestProperties.class);
184+
assertThat(bean.getDescription()).isNull();
185+
}
186+
175187
@Test
176188
public void testNoExceptionOnValidation() {
177189
this.context.register(NoExceptionIfInvalidTestConfiguration.class);
@@ -432,6 +444,12 @@ protected static class ExceptionIfInvalidTestConfiguration {
432444

433445
}
434446

447+
@Configuration
448+
@EnableConfigurationProperties(IgnoredIfInvalidButNotValidatedTestProperties.class)
449+
protected static class IgnoredIfInvalidButNotValidatedTestConfiguration {
450+
451+
}
452+
435453
@Configuration
436454
@EnableConfigurationProperties(NoExceptionIfInvalidTestProperties.class)
437455
protected static class NoExceptionIfInvalidTestConfiguration {
@@ -658,6 +676,7 @@ protected static class IgnoreNestedTestProperties extends TestProperties {
658676
}
659677

660678
@ConfigurationProperties
679+
@Validated
661680
protected static class ExceptionIfInvalidTestProperties extends TestProperties {
662681

663682
@NotNull
@@ -673,7 +692,25 @@ public void setDescription(String description) {
673692

674693
}
675694

695+
@ConfigurationProperties
696+
protected static class IgnoredIfInvalidButNotValidatedTestProperties
697+
extends TestProperties {
698+
699+
@NotNull
700+
private String description;
701+
702+
public String getDescription() {
703+
return this.description;
704+
}
705+
706+
public void setDescription(String description) {
707+
this.description = description;
708+
}
709+
710+
}
711+
676712
@ConfigurationProperties(exceptionIfInvalid = false)
713+
@Validated
677714
protected static class NoExceptionIfInvalidTestProperties extends TestProperties {
678715

679716
@NotNull

spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.boot.diagnostics.FailureAnalysis;
3333
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3434
import org.springframework.context.i18n.LocaleContextHolder;
35+
import org.springframework.validation.annotation.Validated;
3536

3637
import static org.assertj.core.api.Assertions.assertThat;
3738

@@ -90,6 +91,7 @@ static class ValidationFailureConfiguration {
9091
}
9192

9293
@ConfigurationProperties("test.foo")
94+
@Validated
9395
static class ValidationFailureProperties {
9496

9597
@NotNull

0 commit comments

Comments
 (0)