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
pull/3807/head
Andy Wilkinson 9 years ago
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()) { for (String propertyName : source.getPropertyNames()) {
if (includes.matches(propertyName)) { if (includes.matches(propertyName)) {
Object value = getEnumerableProperty(source, resolver, 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) { if (value == null) {
value = source.getProperty(propertyName.toUpperCase()); 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)) { 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) { for (PropertySource<?> source : this.propertySources) {
Object value = source.getProperty(propertyName); Object value = source.getProperty(propertyName);
if (value != null) { if (value != null) {
propertyValue = new PropertyValue(propertyName, value); propertyValue = new OriginCapablePropertyValue(propertyName, value,
propertyName, source);
this.propertyValues.put(propertyName, propertyValue); this.propertyValues.put(propertyName, propertyValue);
return 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.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValue;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.validation.AbstractPropertyBindingResult;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.DataBinder; import org.springframework.validation.DataBinder;
/** /**
@ -115,19 +119,8 @@ public class RelaxedDataBinder extends DataBinder {
return this; return this;
} }
@Override
public void initBeanPropertyAccess() {
super.initBeanPropertyAccess();
// Hook in the RelaxedConversionService
getInternalBindingResult().initConversion(
new RelaxedConversionService(getConversionService()));
}
@Override @Override
protected void doBind(MutablePropertyValues propertyValues) { 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())); super.doBind(modifyProperties(propertyValues, getTarget()));
} }
@ -213,7 +206,9 @@ public class RelaxedDataBinder extends DataBinder {
if (name.startsWith(candidate)) { if (name.startsWith(candidate)) {
name = name.substring(candidate.length()); name = name.substring(candidate.length());
if (!(this.ignoreNestedProperties && name.contains("."))) { 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); 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) { private String initializePath(BeanWrapper wrapper, BeanPath path, int index) {
String prefix = path.prefix(index); String prefix = path.prefix(index);
String key = path.name(index); String key = path.name(index);

@ -309,7 +309,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
return ""; return "";
} }
StringBuilder details = new StringBuilder(); StringBuilder details = new StringBuilder();
details.append("target=").append( details.append("prefix=").append(
(StringUtils.hasLength(annotation.value()) ? annotation.value() (StringUtils.hasLength(annotation.value()) ? annotation.value()
: annotation.prefix())); : annotation.prefix()));
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); 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.annotation.Value;
import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.bind.RelaxedBindingNotWritablePropertyException;
import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -42,6 +43,7 @@ import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; 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 @Test
public void testValidationWithoutJSR303() { public void testValidationWithoutJSR303() {
this.context = new AnnotationConfigApplicationContext(); 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") @ConfigurationProperties(prefix = "test")
public static class PropertyWithJSR303 extends PropertyWithoutJSR303 { public static class PropertyWithJSR303 extends PropertyWithoutJSR303 {

Loading…
Cancel
Save