diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index d1dadfa0ef..4859333b15 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -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`). diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinding.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinding.java new file mode 100644 index 0000000000..8ec2731ebe --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinding.java @@ -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 { +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index 114d698cbe..f88d3a397e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -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> 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> 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 factory = new PropertiesConfigurationFactory( 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 { diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/ConverterBindingTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/ConverterBindingTests.java new file mode 100644 index 0000000000..32122b607e --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/bind/ConverterBindingTests.java @@ -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 converter() { + return new Converter() { + + @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; + } + } + +}