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
pull/8038/head
Phillip Webb 8 years ago
parent f42ebe428c
commit 10dbf3c571

@ -1090,13 +1090,16 @@ only rely on custom converters qualified with `@ConfigurationPropertiesBinding`.
[[boot-features-external-config-validation]] [[boot-features-external-config-validation]]
==== @ConfigurationProperties Validation ==== @ConfigurationProperties Validation
Spring Boot will attempt to validate external configuration, by default using JSR-303 Spring Boot will attempt to validate `@ConfigurationProperties` classes whenever they
(if it is on the classpath). You can simply add JSR-303 `javax.validation` constraint annotated with Spring's `@Validated` annotation. You can use JSR-303 `javax.validation`
annotations to your `@ConfigurationProperties` class: constraint annotations directly on your configuration class. Simply ensure that a
compliant JSR-303 implementation is on your classpath, then add constraint annotations to
your fields:
[source,java,indent=0] [source,java,indent=0]
---- ----
@ConfigurationProperties(prefix="foo") @ConfigurationProperties(prefix="foo")
@Validated
public class FooProperties { public class FooProperties {
@NotNull @NotNull
@ -1114,6 +1117,7 @@ as `@Valid` to trigger its validation. For example, building upon the above
[source,java,indent=0] [source,java,indent=0]
---- ----
@ConfigurationProperties(prefix="connection") @ConfigurationProperties(prefix="connection")
@Validated
public class FooProperties { public class FooProperties {
@NotNull @NotNull

@ -24,6 +24,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- Test --> <!-- Test -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

@ -0,0 +1,39 @@
/*
* 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 sample.simple;
import javax.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sample")
public class SampleConfigurationProperties {
@NotNull
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}

@ -58,6 +58,10 @@ public class SampleSimpleApplicationTests {
SampleSimpleApplication.main(new String[0]); SampleSimpleApplication.main(new String[0]);
String output = this.outputCapture.toString(); String output = this.outputCapture.toString();
assertThat(output).contains("Hello Phil"); assertThat(output).contains("Hello Phil");
assertThat(output).contains("The @ConfigurationProperties bean class "
+ "sample.simple.SampleConfigurationProperties contains "
+ "validation constraints but had not been annotated "
+ "with @Validated");
} }
@Test @Test

@ -267,8 +267,9 @@ public class PropertiesConfigurationFactory<T>
relaxedTargetNames); relaxedTargetNames);
dataBinder.bind(propertyValues); dataBinder.bind(propertyValues);
if (this.validator != null) { if (this.validator != null) {
validate(dataBinder); dataBinder.validate();
} }
checkForBindingErrors(dataBinder);
} }
private Iterable<String> getRelaxedTargetNames() { private Iterable<String> getRelaxedTargetNames() {
@ -338,8 +339,8 @@ public class PropertiesConfigurationFactory<T>
return this.target != null && Map.class.isAssignableFrom(this.target.getClass()); return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
} }
private void validate(RelaxedDataBinder dataBinder) throws BindException { private void checkForBindingErrors(RelaxedDataBinder dataBinder)
dataBinder.validate(); throws BindException {
BindingResult errors = dataBinder.getBindingResult(); BindingResult errors = dataBinder.getBindingResult();
if (errors.hasErrors()) { if (errors.hasErrors()) {
logger.error("Properties configuration failed validation"); logger.error("Properties configuration failed validation");

@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AliasFor;
import org.springframework.validation.annotation.Validated;
/** /**
* Annotation for externalized configuration. Add this to a class definition or a * Annotation for externalized configuration. Add this to a class definition or a
@ -80,9 +81,10 @@ public @interface ConfigurationProperties {
boolean ignoreUnknownFields() default true; boolean ignoreUnknownFields() default true;
/** /**
* Flag to indicate that an exception should be raised if a Validator is available and * Flag to indicate that an exception should be raised if a Validator is available,
* validation fails. If it is set to false, validation errors will be swallowed. They * the class is annotated with {@link Validated @Validated} and validation fails. If
* will be logged, but not propagated to the caller. * it is set to false, validation errors will be swallowed. They will be logged, but
* not propagated to the caller.
* @return the flag value (default true) * @return the flag value (default true)
*/ */
boolean exceptionIfInvalid() default true; boolean exceptionIfInvalid() default true;

@ -45,6 +45,7 @@ import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered; import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
@ -61,6 +62,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/** /**
@ -362,8 +364,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
return this.validator; return this.validator;
} }
if (this.localValidator == null && isJsr303Present()) { if (this.localValidator == null && isJsr303Present()) {
this.localValidator = new LocalValidatorFactory() this.localValidator = new ValidatedLocalValidatorFactoryBean(
.run(this.applicationContext); this.applicationContext);
} }
return this.localValidator; return this.localValidator;
} }
@ -394,18 +396,38 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
} }
/** /**
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent class * {@link LocalValidatorFactoryBean} supports classes annotated with
* loader issues. * {@link Validated @Validated}.
*/ */
private static class LocalValidatorFactory { private static class ValidatedLocalValidatorFactoryBean
extends LocalValidatorFactoryBean {
public Validator run(ApplicationContext applicationContext) {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); private static final Log logger = LogFactory
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); .getLog(ConfigurationPropertiesBindingPostProcessor.class);
validator.setApplicationContext(applicationContext);
validator.setMessageInterpolator(interpolatorFactory.getObject()); ValidatedLocalValidatorFactoryBean(ApplicationContext applicationContext) {
validator.afterPropertiesSet(); setApplicationContext(applicationContext);
return validator; setMessageInterpolator(new MessageInterpolatorFactory().getObject());
afterPropertiesSet();
}
@Override
public boolean supports(Class<?> type) {
if (!super.supports(type)) {
return false;
}
if (AnnotatedElementUtils.isAnnotated(type, Validated.class)) {
return true;
}
if (type.getPackage().getName().startsWith("org.springframework.boot")) {
return false;
}
if (getConstraintsForClass(type).isBeanConstrained()) {
logger.warn("The @ConfigurationProperties bean " + type
+ " contains validation constraints but had not been annotated "
+ "with @Validated.");
}
return true;
} }
} }

@ -46,6 +46,7 @@ import org.springframework.validation.BindException;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils; import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -456,6 +457,7 @@ public class ConfigurationPropertiesBindingPostProcessorTests {
} }
@ConfigurationProperties(prefix = "test") @ConfigurationProperties(prefix = "test")
@Validated
public static class PropertyWithJSR303 extends PropertyWithoutJSR303 { public static class PropertyWithJSR303 extends PropertyWithoutJSR303 {
@NotNull @NotNull

@ -38,6 +38,7 @@ import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -172,6 +173,17 @@ public class EnableConfigurationPropertiesTests {
this.context.refresh(); this.context.refresh();
} }
@Test
public void testNoExceptionOnValidationWithoutValidated() {
this.context.register(IgnoredIfInvalidButNotValidatedTestConfiguration.class);
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"name:foo");
this.context.refresh();
IgnoredIfInvalidButNotValidatedTestProperties bean = this.context
.getBean(IgnoredIfInvalidButNotValidatedTestProperties.class);
assertThat(bean.getDescription()).isNull();
}
@Test @Test
public void testNoExceptionOnValidation() { public void testNoExceptionOnValidation() {
this.context.register(NoExceptionIfInvalidTestConfiguration.class); this.context.register(NoExceptionIfInvalidTestConfiguration.class);
@ -432,6 +444,12 @@ public class EnableConfigurationPropertiesTests {
} }
@Configuration
@EnableConfigurationProperties(IgnoredIfInvalidButNotValidatedTestProperties.class)
protected static class IgnoredIfInvalidButNotValidatedTestConfiguration {
}
@Configuration @Configuration
@EnableConfigurationProperties(NoExceptionIfInvalidTestProperties.class) @EnableConfigurationProperties(NoExceptionIfInvalidTestProperties.class)
protected static class NoExceptionIfInvalidTestConfiguration { protected static class NoExceptionIfInvalidTestConfiguration {
@ -658,6 +676,7 @@ public class EnableConfigurationPropertiesTests {
} }
@ConfigurationProperties @ConfigurationProperties
@Validated
protected static class ExceptionIfInvalidTestProperties extends TestProperties { protected static class ExceptionIfInvalidTestProperties extends TestProperties {
@NotNull @NotNull
@ -673,7 +692,25 @@ public class EnableConfigurationPropertiesTests {
} }
@ConfigurationProperties
protected static class IgnoredIfInvalidButNotValidatedTestProperties
extends TestProperties {
@NotNull
private String description;
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
}
@ConfigurationProperties(exceptionIfInvalid = false) @ConfigurationProperties(exceptionIfInvalid = false)
@Validated
protected static class NoExceptionIfInvalidTestProperties extends TestProperties { protected static class NoExceptionIfInvalidTestProperties extends TestProperties {
@NotNull @NotNull

@ -32,6 +32,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -90,6 +91,7 @@ public class BindFailureAnalyzerTests {
} }
@ConfigurationProperties("test.foo") @ConfigurationProperties("test.foo")
@Validated
static class ValidationFailureProperties { static class ValidationFailureProperties {
@NotNull @NotNull

Loading…
Cancel
Save