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:
parent
538afc4ab1
commit
67483bb73c
|
|
@ -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`).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue