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
This commit is contained in:
Phillip Webb 2017-01-18 20:29:31 -08:00
parent f42ebe428c
commit 10dbf3c571
12 changed files with 141 additions and 23 deletions

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -1 +1,3 @@
name: Phil
name=Phil
sample.name=Andy

View File

@ -58,6 +58,10 @@ public class SampleSimpleApplicationTests {
SampleSimpleApplication.main(new String[0]);
String output = this.outputCapture.toString();
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

View File

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

View File

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

View File

@ -45,6 +45,7 @@ import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
@ -61,6 +62,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/**
@ -362,8 +364,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
return this.validator;
}
if (this.localValidator == null && isJsr303Present()) {
this.localValidator = new LocalValidatorFactory()
.run(this.applicationContext);
this.localValidator = new ValidatedLocalValidatorFactoryBean(
this.applicationContext);
}
return this.localValidator;
}
@ -394,18 +396,38 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
}
/**
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent class
* loader issues.
* {@link LocalValidatorFactoryBean} supports classes annotated with
* {@link Validated @Validated}.
*/
private static class LocalValidatorFactory {
private static class ValidatedLocalValidatorFactoryBean
extends LocalValidatorFactoryBean {
public Validator run(ApplicationContext applicationContext) {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
validator.setApplicationContext(applicationContext);
validator.setMessageInterpolator(interpolatorFactory.getObject());
validator.afterPropertiesSet();
return validator;
private static final Log logger = LogFactory
.getLog(ConfigurationPropertiesBindingPostProcessor.class);
ValidatedLocalValidatorFactoryBean(ApplicationContext applicationContext) {
setApplicationContext(applicationContext);
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;
}
}

View File

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

View File

@ -38,6 +38,7 @@ import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Component;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat;
@ -172,6 +173,17 @@ public class EnableConfigurationPropertiesTests {
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
public void testNoExceptionOnValidation() {
this.context.register(NoExceptionIfInvalidTestConfiguration.class);
@ -432,6 +444,12 @@ public class EnableConfigurationPropertiesTests {
}
@Configuration
@EnableConfigurationProperties(IgnoredIfInvalidButNotValidatedTestProperties.class)
protected static class IgnoredIfInvalidButNotValidatedTestConfiguration {
}
@Configuration
@EnableConfigurationProperties(NoExceptionIfInvalidTestProperties.class)
protected static class NoExceptionIfInvalidTestConfiguration {
@ -658,6 +676,7 @@ public class EnableConfigurationPropertiesTests {
}
@ConfigurationProperties
@Validated
protected static class ExceptionIfInvalidTestProperties extends TestProperties {
@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)
@Validated
protected static class NoExceptionIfInvalidTestProperties extends TestProperties {
@NotNull

View File

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