Include information about a property’s origin in binding failures
This commit enhances RelaxedDataBinder to include information about the origin of a property (its original name before any prefix was removed and its source) when it encounters an unwritable property. For example, launching an application with a SERVER_HOME environment variable configured will produce the following failure message: Failed to bind 'SERVER_HOME' from 'systemEnvironment' to 'HOME' property on 'org.springframework.boot.autoconfigure.web.ServerProperties' Closes gh-3778
This commit is contained in:
parent
48f16c4386
commit
6bd6bc9e10
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright 2012-2015 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 org.springframework.boot.bind;
|
||||
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
/**
|
||||
* A {@link PropertyValue} that can provide information about its origin.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class OriginCapablePropertyValue extends PropertyValue {
|
||||
|
||||
private static final String ATTRIBUTE_PROPERTY_ORIGIN = "propertyOrigin";
|
||||
|
||||
private final PropertyOrigin origin;
|
||||
|
||||
public OriginCapablePropertyValue(PropertyValue propertyValue) {
|
||||
this(propertyValue.getName(), propertyValue.getValue(),
|
||||
(PropertyOrigin) propertyValue.getAttribute(ATTRIBUTE_PROPERTY_ORIGIN));
|
||||
}
|
||||
|
||||
public OriginCapablePropertyValue(String name, Object value, String originName,
|
||||
PropertySource<?> originSource) {
|
||||
this(name, value, new PropertyOrigin(originSource, originName));
|
||||
}
|
||||
|
||||
public OriginCapablePropertyValue(String name, Object value, PropertyOrigin origin) {
|
||||
super(name, value);
|
||||
this.origin = origin;
|
||||
setAttribute(ATTRIBUTE_PROPERTY_ORIGIN, origin);
|
||||
}
|
||||
|
||||
public PropertyOrigin getOrigin() {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String name = this.origin != null ? this.origin.getName() : this.getName();
|
||||
String source = this.origin.getSource() != null ? this.origin.getSource()
|
||||
.getName() : "unknown";
|
||||
return "'" + name + "' from '" + source + "'";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2012-2015 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 org.springframework.boot.bind;
|
||||
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
/**
|
||||
* The origin of a property, specifically its source and its name before any prefix was
|
||||
* removed.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class PropertyOrigin {
|
||||
|
||||
private final PropertySource<?> source;
|
||||
|
||||
private final String name;
|
||||
|
||||
PropertyOrigin(PropertySource<?> source, String name) {
|
||||
this.name = name;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public PropertySource<?> getSource() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ public class PropertySourcesPropertyValues implements PropertyValues {
|
|||
for (String propertyName : source.getPropertyNames()) {
|
||||
if (includes.matches(propertyName)) {
|
||||
Object value = getEnumerableProperty(source, resolver, propertyName);
|
||||
putIfAbsent(propertyName, value);
|
||||
putIfAbsent(propertyName, value, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,13 +155,14 @@ public class PropertySourcesPropertyValues implements PropertyValues {
|
|||
if (value == null) {
|
||||
value = source.getProperty(propertyName.toUpperCase());
|
||||
}
|
||||
putIfAbsent(propertyName, value);
|
||||
putIfAbsent(propertyName, value, source);
|
||||
}
|
||||
}
|
||||
|
||||
private void putIfAbsent(String propertyName, Object value) {
|
||||
private void putIfAbsent(String propertyName, Object value, PropertySource<?> source) {
|
||||
if (value != null && !this.propertyValues.containsKey(propertyName)) {
|
||||
this.propertyValues.put(propertyName, new PropertyValue(propertyName, value));
|
||||
this.propertyValues.put(propertyName, new OriginCapablePropertyValue(
|
||||
propertyName, value, propertyName, source));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +181,8 @@ public class PropertySourcesPropertyValues implements PropertyValues {
|
|||
for (PropertySource<?> source : this.propertySources) {
|
||||
Object value = source.getProperty(propertyName);
|
||||
if (value != null) {
|
||||
propertyValue = new PropertyValue(propertyName, value);
|
||||
propertyValue = new OriginCapablePropertyValue(propertyName, value,
|
||||
propertyName, source);
|
||||
this.propertyValues.put(propertyName, propertyValue);
|
||||
return propertyValue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2012-2015 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 org.springframework.boot.bind;
|
||||
|
||||
import org.springframework.beans.NotWritablePropertyException;
|
||||
|
||||
/**
|
||||
* A custom {@link NotWritablePropertyException} that is thrown when a failure occurs
|
||||
* during relaxed binding
|
||||
*
|
||||
* @see RelaxedDataBinder
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class RelaxedBindingNotWritablePropertyException extends
|
||||
NotWritablePropertyException {
|
||||
|
||||
private final String message;
|
||||
|
||||
private final PropertyOrigin propertyOrigin;
|
||||
|
||||
RelaxedBindingNotWritablePropertyException(NotWritablePropertyException ex,
|
||||
PropertyOrigin propertyOrigin) {
|
||||
super(ex.getBeanClass(), ex.getPropertyName());
|
||||
this.propertyOrigin = propertyOrigin;
|
||||
this.message = "Failed to bind '" + propertyOrigin.getName() + "' from '"
|
||||
+ propertyOrigin.getSource().getName() + "' to '" + ex.getPropertyName()
|
||||
+ "' property on '" + ex.getBeanClass().getName() + "'";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public PropertyOrigin getPropertyOrigin() {
|
||||
return this.propertyOrigin;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -29,13 +29,17 @@ import java.util.Set;
|
|||
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.InvalidPropertyException;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.NotWritablePropertyException;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.AbstractPropertyBindingResult;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.DataBinder;
|
||||
|
||||
/**
|
||||
|
|
@ -115,19 +119,8 @@ public class RelaxedDataBinder extends DataBinder {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initBeanPropertyAccess() {
|
||||
super.initBeanPropertyAccess();
|
||||
// Hook in the RelaxedConversionService
|
||||
getInternalBindingResult().initConversion(
|
||||
new RelaxedConversionService(getConversionService()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doBind(MutablePropertyValues propertyValues) {
|
||||
// Harmless additional property editor comes in very handy sometimes...
|
||||
getPropertyEditorRegistry().registerCustomEditor(InetAddress.class,
|
||||
new InetAddressEditor());
|
||||
super.doBind(modifyProperties(propertyValues, getTarget()));
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +206,9 @@ public class RelaxedDataBinder extends DataBinder {
|
|||
if (name.startsWith(candidate)) {
|
||||
name = name.substring(candidate.length());
|
||||
if (!(this.ignoreNestedProperties && name.contains("."))) {
|
||||
rtn.add(name, value.getValue());
|
||||
PropertyOrigin propertyOrigin = findPropertyOrigin(value);
|
||||
rtn.addPropertyValue(new OriginCapablePropertyValue(name, value
|
||||
.getValue(), propertyOrigin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,6 +240,44 @@ public class RelaxedDataBinder extends DataBinder {
|
|||
return initializePath(wrapper, new BeanPath(path), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
|
||||
return new BeanPropertyBindingResult(getTarget(), getObjectName(),
|
||||
isAutoGrowNestedPaths(), getAutoGrowCollectionLimit()) {
|
||||
@Override
|
||||
protected BeanWrapper createBeanWrapper() {
|
||||
BeanWrapper beanWrapper = new BeanWrapperImpl(getTarget()) {
|
||||
@Override
|
||||
public void setPropertyValue(PropertyValue pv) throws BeansException {
|
||||
try {
|
||||
super.setPropertyValue(pv);
|
||||
}
|
||||
catch (NotWritablePropertyException ex) {
|
||||
PropertyOrigin origin = findPropertyOrigin(pv);
|
||||
if (origin != null) {
|
||||
throw new RelaxedBindingNotWritablePropertyException(ex,
|
||||
origin);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
};
|
||||
beanWrapper.setConversionService(new RelaxedConversionService(
|
||||
getConversionService()));
|
||||
beanWrapper.registerCustomEditor(InetAddress.class,
|
||||
new InetAddressEditor());
|
||||
return beanWrapper;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private PropertyOrigin findPropertyOrigin(PropertyValue propertyValue) {
|
||||
if (propertyValue instanceof OriginCapablePropertyValue) {
|
||||
return ((OriginCapablePropertyValue) propertyValue).getOrigin();
|
||||
}
|
||||
return new OriginCapablePropertyValue(propertyValue).getOrigin();
|
||||
}
|
||||
|
||||
private String initializePath(BeanWrapper wrapper, BeanPath path, int index) {
|
||||
String prefix = path.prefix(index);
|
||||
String key = path.name(index);
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
|
|||
return "";
|
||||
}
|
||||
StringBuilder details = new StringBuilder();
|
||||
details.append("target=").append(
|
||||
details.append("prefix=").append(
|
||||
(StringUtils.hasLength(annotation.value()) ? annotation.value()
|
||||
: annotation.prefix()));
|
||||
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import org.springframework.beans.factory.InitializingBean;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.boot.bind.RelaxedBindingNotWritablePropertyException;
|
||||
import org.springframework.boot.test.EnvironmentTestUtils;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
|
@ -42,6 +43,7 @@ import org.springframework.validation.ValidationUtils;
|
|||
import org.springframework.validation.Validator;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
|
@ -85,6 +87,23 @@ public class ConfigurationPropertiesBindingPostProcessorTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknonwFieldFailureMessageContainsDetailsOfPropertyOrigin() {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
EnvironmentTestUtils.addEnvironment(this.context, "com.example.baz:spam");
|
||||
this.context.register(TestConfiguration.class);
|
||||
try {
|
||||
this.context.refresh();
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (BeanCreationException ex) {
|
||||
RelaxedBindingNotWritablePropertyException bex = (RelaxedBindingNotWritablePropertyException) ex.getRootCause();
|
||||
assertThat(bex.getMessage(),
|
||||
startsWith("Failed to bind 'com.example.baz' from 'test' to 'baz' "
|
||||
+ "property on '" + TestConfiguration.class.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidationWithoutJSR303() {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
|
|
@ -402,6 +421,23 @@ public class ConfigurationPropertiesBindingPostProcessorTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties
|
||||
@ConfigurationProperties(prefix = "com.example", ignoreUnknownFields = false)
|
||||
public static class TestConfiguration {
|
||||
|
||||
private String bar;
|
||||
|
||||
public void setBar(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
public String getBar() {
|
||||
return this.bar;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
public static class PropertyWithJSR303 extends PropertyWithoutJSR303 {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue