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
it binds to the `@ConfigurationProperties` beans. If you need custom type conversion you
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;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.bind.PropertiesConfigurationFactory;
import org.springframework.boot.env.PropertySourcesLoader;
@ -69,13 +72,13 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
* @author Stephane Nicoll
*/
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware,
InitializingBean, DisposableBean, PriorityOrdered {
BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware,
InitializingBean, DisposableBean, PriorityOrdered {
public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
private static final String[] VALIDATOR_CLASSES = { "javax.validation.Validator",
"javax.validation.ValidatorFactory" };
"javax.validation.ValidatorFactory" };
private ConfigurationBeanFactoryMetaData beans = new ConfigurationBeanFactoryMetaData();
@ -97,8 +100,21 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private ApplicationContext applicationContext;
private List<Converter<?, ?>> converters = Collections.emptyList();
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
*/
@ -246,8 +262,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(
bean.getClass(), ConfigurationProperties.class);
ConfigurationProperties annotation = AnnotationUtils
.findAnnotation(bean.getClass(), ConfigurationProperties.class);
if (annotation != null || bean instanceof ConfigurationPropertiesHolder) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
@ -267,13 +283,13 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private void postProcessBeforeInitialization(Object bean, String beanName,
ConfigurationProperties annotation) {
Object target = (bean instanceof ConfigurationPropertiesHolder ? ((ConfigurationPropertiesHolder) bean)
.getTarget() : bean);
Object target = (bean instanceof ConfigurationPropertiesHolder
? ((ConfigurationPropertiesHolder) bean).getTarget() : bean);
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
target);
if (annotation != null && annotation.locations().length != 0) {
factory.setPropertySources(loadPropertySources(annotation.locations(),
annotation.merge()));
factory.setPropertySources(
loadPropertySources(annotation.locations(), annotation.merge()));
}
else {
factory.setPropertySources(this.propertySources);
@ -281,15 +297,15 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
factory.setValidator(determineValidator(bean));
// If no explicit conversion service is provided we add one so that (at least)
// comma-separated arrays of convertibles can be bound automatically
factory.setConversionService(this.conversionService == null ? getDefaultConversionService()
: this.conversionService);
factory.setConversionService(this.conversionService == null
? getDefaultConversionService() : this.conversionService);
if (annotation != null) {
factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
String targetName = (StringUtils.hasLength(annotation.value()) ? annotation
.value() : annotation.prefix());
String targetName = (StringUtils.hasLength(annotation.value())
? annotation.value() : annotation.prefix());
if (StringUtils.hasLength(targetName)) {
factory.setTargetName(targetName);
}
@ -300,7 +316,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
catch (Exception ex) {
String targetClass = ClassUtils.getShortName(target.getClass());
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 "";
}
StringBuilder details = new StringBuilder();
details.append("prefix=").append(
(StringUtils.hasLength(annotation.value()) ? annotation.value()
: annotation.prefix()));
details.append("prefix=").append((StringUtils.hasLength(annotation.value())
? annotation.value() : annotation.prefix()));
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields());
details.append(", ignoreNestedProperties=").append(
annotation.ignoreNestedProperties());
details.append(", ignoreNestedProperties=")
.append(annotation.ignoreNestedProperties());
return details.toString();
}
private Validator determineValidator(Object bean) {
boolean globalValidatorSupportBean = (this.validator != null && this.validator
.supports(bean.getClass()));
boolean globalValidatorSupportBean = (this.validator != null
&& this.validator.supports(bean.getClass()));
if (ClassUtils.isAssignable(Validator.class, bean.getClass())) {
if (!globalValidatorSupportBean) {
return (Validator) bean;
@ -336,8 +352,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
try {
PropertySourcesLoader loader = new PropertySourcesLoader();
for (String location : locations) {
Resource resource = this.resourceLoader.getResource(this.environment
.resolvePlaceholders(location));
Resource resource = this.resourceLoader
.getResource(this.environment.resolvePlaceholders(location));
String[] profiles = this.environment.getActiveProfiles();
for (int i = profiles.length; i-- > 0;) {
String profile = profiles[i];
@ -361,8 +377,9 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private ConversionService getDefaultConversionService() {
if (this.defaultConversionService == null) {
DefaultConversionService conversionService = new DefaultConversionService();
for (Converter<?, ?> converter : ((ListableBeanFactory) this.beanFactory)
.getBeansOfType(Converter.class, false, false).values()) {
this.applicationContext.getAutowireCapableBeanFactory()
.autowireBean(this);
for (Converter<?, ?> converter : this.converters) {
conversionService.addConverter(converter);
}
this.defaultConversionService = conversionService;
@ -371,8 +388,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
}
/**
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent class
* loader issues.
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent
* class loader issues.
*/
private static class Jsr303ValidatorFactory {
@ -386,8 +403,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
}
/**
* {@link Validator} implementation that wraps {@link Validator} instances and chains
* their execution.
* {@link Validator} implementation that wraps {@link Validator} instances and
* chains their execution.
*/
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
* 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 {

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