Only use Converters which are @ConfigurationPropertiesBinder qualified

Users sometimes create beans of type Converter and don't expect that to
automatically trigger a cascade of early initialization. This change adds
a qualifier to the Converters that are used by @ConfigurationProperties,
so they can be isolated (and simple).

Fixes gh-2669
This commit is contained in:
Dave Syer 2015-08-26 14:56:21 +01:00
parent 538afc4ab1
commit 67483bb73c
4 changed files with 198 additions and 31 deletions

View File

@ -761,7 +761,8 @@ The following properties names can all be used:
Spring will attempt to coerce the external application properties to the right type when Spring will attempt to coerce the external application properties to the right type when
it binds to the `@ConfigurationProperties` beans. If you need custom type conversion you it binds to the `@ConfigurationProperties` beans. If you need custom type conversion you
can provide a `ConversionService` bean (with bean id `conversionService`) or custom can provide a `ConversionService` bean (with bean id `conversionService`) or custom
property editors (via a `CustomEditorConfigurer` bean). property editors (via a `CustomEditorConfigurer` bean) or custom `Converters` (with
bean definitions annotated as `@ConfigurationPropertiesBinding`).

View File

@ -0,0 +1,38 @@
/*
* 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.context.properties;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
/**
* Qualifier for beans that are needed to configure the binding of
* {@link ConfigurationProperties} (e.g. Converters).
*
* @author Dave Syer
*/
@Qualifier
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertiesBinding {
}

View File

@ -17,7 +17,9 @@
package org.springframework.boot.context.properties; package org.springframework.boot.context.properties;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
@ -28,6 +30,7 @@ import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.boot.bind.PropertiesConfigurationFactory;
import org.springframework.boot.env.PropertySourcesLoader; import org.springframework.boot.env.PropertySourcesLoader;
@ -69,8 +72,8 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware,
InitializingBean, DisposableBean, PriorityOrdered { InitializingBean, DisposableBean, PriorityOrdered {
public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
@ -97,8 +100,21 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
private List<Converter<?, ?>> converters = Collections.emptyList();
private int order = Ordered.HIGHEST_PRECEDENCE + 1; private int order = Ordered.HIGHEST_PRECEDENCE + 1;
/**
* A list of custom converters (in addition to the defaults) to use when
* converting properties for binding.
* @param converters the converters to set
*/
@Autowired(required = false)
@ConfigurationPropertiesBinding
public void setConverters(List<Converter<?, ?>> converters) {
this.converters = converters;
}
/** /**
* @param order the order to set * @param order the order to set
*/ */
@ -246,8 +262,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
@Override @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException { throws BeansException {
ConfigurationProperties annotation = AnnotationUtils.findAnnotation( ConfigurationProperties annotation = AnnotationUtils
bean.getClass(), ConfigurationProperties.class); .findAnnotation(bean.getClass(), ConfigurationProperties.class);
if (annotation != null || bean instanceof ConfigurationPropertiesHolder) { if (annotation != null || bean instanceof ConfigurationPropertiesHolder) {
postProcessBeforeInitialization(bean, beanName, annotation); postProcessBeforeInitialization(bean, beanName, annotation);
} }
@ -267,13 +283,13 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private void postProcessBeforeInitialization(Object bean, String beanName, private void postProcessBeforeInitialization(Object bean, String beanName,
ConfigurationProperties annotation) { ConfigurationProperties annotation) {
Object target = (bean instanceof ConfigurationPropertiesHolder ? ((ConfigurationPropertiesHolder) bean) Object target = (bean instanceof ConfigurationPropertiesHolder
.getTarget() : bean); ? ((ConfigurationPropertiesHolder) bean).getTarget() : bean);
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>( PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
target); target);
if (annotation != null && annotation.locations().length != 0) { if (annotation != null && annotation.locations().length != 0) {
factory.setPropertySources(loadPropertySources(annotation.locations(), factory.setPropertySources(
annotation.merge())); loadPropertySources(annotation.locations(), annotation.merge()));
} }
else { else {
factory.setPropertySources(this.propertySources); factory.setPropertySources(this.propertySources);
@ -281,15 +297,15 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
factory.setValidator(determineValidator(bean)); factory.setValidator(determineValidator(bean));
// If no explicit conversion service is provided we add one so that (at least) // If no explicit conversion service is provided we add one so that (at least)
// comma-separated arrays of convertibles can be bound automatically // comma-separated arrays of convertibles can be bound automatically
factory.setConversionService(this.conversionService == null ? getDefaultConversionService() factory.setConversionService(this.conversionService == null
: this.conversionService); ? getDefaultConversionService() : this.conversionService);
if (annotation != null) { if (annotation != null) {
factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields()); factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields()); factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
factory.setExceptionIfInvalid(annotation.exceptionIfInvalid()); factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties()); factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
String targetName = (StringUtils.hasLength(annotation.value()) ? annotation String targetName = (StringUtils.hasLength(annotation.value())
.value() : annotation.prefix()); ? annotation.value() : annotation.prefix());
if (StringUtils.hasLength(targetName)) { if (StringUtils.hasLength(targetName)) {
factory.setTargetName(targetName); factory.setTargetName(targetName);
} }
@ -300,7 +316,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
catch (Exception ex) { catch (Exception ex) {
String targetClass = ClassUtils.getShortName(target.getClass()); String targetClass = ClassUtils.getShortName(target.getClass());
throw new BeanCreationException(beanName, "Could not bind properties to " throw new BeanCreationException(beanName, "Could not bind properties to "
+ targetClass + " (" + getAnnotationDetails(annotation) + ")", ex); + targetClass + " (" + getAnnotationDetails(annotation) + ")",
ex);
} }
} }
@ -309,19 +326,18 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
return ""; return "";
} }
StringBuilder details = new StringBuilder(); StringBuilder details = new StringBuilder();
details.append("prefix=").append( details.append("prefix=").append((StringUtils.hasLength(annotation.value())
(StringUtils.hasLength(annotation.value()) ? annotation.value() ? annotation.value() : annotation.prefix()));
: annotation.prefix()));
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields()); details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields());
details.append(", ignoreNestedProperties=").append( details.append(", ignoreNestedProperties=")
annotation.ignoreNestedProperties()); .append(annotation.ignoreNestedProperties());
return details.toString(); return details.toString();
} }
private Validator determineValidator(Object bean) { private Validator determineValidator(Object bean) {
boolean globalValidatorSupportBean = (this.validator != null && this.validator boolean globalValidatorSupportBean = (this.validator != null
.supports(bean.getClass())); && this.validator.supports(bean.getClass()));
if (ClassUtils.isAssignable(Validator.class, bean.getClass())) { if (ClassUtils.isAssignable(Validator.class, bean.getClass())) {
if (!globalValidatorSupportBean) { if (!globalValidatorSupportBean) {
return (Validator) bean; return (Validator) bean;
@ -336,8 +352,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
try { try {
PropertySourcesLoader loader = new PropertySourcesLoader(); PropertySourcesLoader loader = new PropertySourcesLoader();
for (String location : locations) { for (String location : locations) {
Resource resource = this.resourceLoader.getResource(this.environment Resource resource = this.resourceLoader
.resolvePlaceholders(location)); .getResource(this.environment.resolvePlaceholders(location));
String[] profiles = this.environment.getActiveProfiles(); String[] profiles = this.environment.getActiveProfiles();
for (int i = profiles.length; i-- > 0;) { for (int i = profiles.length; i-- > 0;) {
String profile = profiles[i]; String profile = profiles[i];
@ -361,8 +377,9 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private ConversionService getDefaultConversionService() { private ConversionService getDefaultConversionService() {
if (this.defaultConversionService == null) { if (this.defaultConversionService == null) {
DefaultConversionService conversionService = new DefaultConversionService(); DefaultConversionService conversionService = new DefaultConversionService();
for (Converter<?, ?> converter : ((ListableBeanFactory) this.beanFactory) this.applicationContext.getAutowireCapableBeanFactory()
.getBeansOfType(Converter.class, false, false).values()) { .autowireBean(this);
for (Converter<?, ?> converter : this.converters) {
conversionService.addConverter(converter); conversionService.addConverter(converter);
} }
this.defaultConversionService = conversionService; this.defaultConversionService = conversionService;
@ -371,8 +388,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
} }
/** /**
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent class * Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent
* loader issues. * class loader issues.
*/ */
private static class Jsr303ValidatorFactory { private static class Jsr303ValidatorFactory {
@ -386,8 +403,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
} }
/** /**
* {@link Validator} implementation that wraps {@link Validator} instances and chains * {@link Validator} implementation that wraps {@link Validator} instances and
* their execution. * chains their execution.
*/ */
private static class ChainingValidator implements Validator { private static class ChainingValidator implements Validator {
@ -421,7 +438,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
/** /**
* Convenience class to flatten out a tree of property sources without losing the * Convenience class to flatten out a tree of property sources without losing the
* reference to the backing data (which can therefore be updated in the background). * reference to the backing data (which can therefore be updated in the
* background).
*/ */
private static class FlatPropertySources implements PropertySources { private static class FlatPropertySources implements PropertySources {

View File

@ -0,0 +1,110 @@
/*
* Copyright 2012-2014 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 static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.bind.ConverterBindingTests.TestConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.converter.Converter;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests for {@link ConfigurationProperties} binding with custom converters.
*
* @author Dave Syer
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(TestConfig.class)
@IntegrationTest("foo=bar")
public class ConverterBindingTests {
@Value("${foo:}")
private String foo;
@Autowired
private Wrapper properties;
@Test
public void overridingOfPropertiesOrderOfAtPropertySources() {
assertThat(this.properties.getFoo().getName(), is(this.foo));
}
@Configuration
@EnableConfigurationProperties(Wrapper.class)
public static class TestConfig {
@Bean
@ConfigurationPropertiesBinding
public Converter<String, Foo> converter() {
return new Converter<String, ConverterBindingTests.Foo>() {
@Override
public Foo convert(String source) {
Foo foo = new Foo();
foo.setName(source);
return foo;
}
};
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
public static class Foo {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
@ConfigurationProperties
public static class Wrapper {
private Foo foo;
public Foo getFoo() {
return this.foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
}
}