Skip to content

Commit 67483bb

Browse files
committed
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
1 parent 538afc4 commit 67483bb

File tree

4 files changed

+198
-31
lines changed

4 files changed

+198
-31
lines changed

spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

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

766767

767768

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2012-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.beans.factory.annotation.Qualifier;
26+
27+
/**
28+
* Qualifier for beans that are needed to configure the binding of
29+
* {@link ConfigurationProperties} (e.g. Converters).
30+
*
31+
* @author Dave Syer
32+
*/
33+
@Qualifier
34+
@Target({ ElementType.TYPE, ElementType.METHOD })
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Documented
37+
public @interface ConfigurationPropertiesBinding {
38+
}

spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java

+48-30
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package org.springframework.boot.context.properties;
1818

1919
import java.io.IOException;
20+
import java.util.Collections;
2021
import java.util.Iterator;
22+
import java.util.List;
2123
import java.util.Map;
2224

2325
import org.springframework.beans.BeansException;
@@ -28,6 +30,7 @@
2830
import org.springframework.beans.factory.InitializingBean;
2931
import org.springframework.beans.factory.ListableBeanFactory;
3032
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
33+
import org.springframework.beans.factory.annotation.Autowired;
3134
import org.springframework.beans.factory.config.BeanPostProcessor;
3235
import org.springframework.boot.bind.PropertiesConfigurationFactory;
3336
import org.springframework.boot.env.PropertySourcesLoader;
@@ -69,13 +72,13 @@
6972
* @author Stephane Nicoll
7073
*/
7174
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
72-
BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware,
73-
InitializingBean, DisposableBean, PriorityOrdered {
75+
BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware,
76+
InitializingBean, DisposableBean, PriorityOrdered {
7477

7578
public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
7679

7780
private static final String[] VALIDATOR_CLASSES = { "javax.validation.Validator",
78-
"javax.validation.ValidatorFactory" };
81+
"javax.validation.ValidatorFactory" };
7982

8083
private ConfigurationBeanFactoryMetaData beans = new ConfigurationBeanFactoryMetaData();
8184

@@ -97,8 +100,21 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
97100

98101
private ApplicationContext applicationContext;
99102

103+
private List<Converter<?, ?>> converters = Collections.emptyList();
104+
100105
private int order = Ordered.HIGHEST_PRECEDENCE + 1;
101106

107+
/**
108+
* A list of custom converters (in addition to the defaults) to use when
109+
* converting properties for binding.
110+
* @param converters the converters to set
111+
*/
112+
@Autowired(required = false)
113+
@ConfigurationPropertiesBinding
114+
public void setConverters(List<Converter<?, ?>> converters) {
115+
this.converters = converters;
116+
}
117+
102118
/**
103119
* @param order the order to set
104120
*/
@@ -246,8 +262,8 @@ private <T> T getOptionalBean(String name, Class<T> type) {
246262
@Override
247263
public Object postProcessBeforeInitialization(Object bean, String beanName)
248264
throws BeansException {
249-
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(
250-
bean.getClass(), ConfigurationProperties.class);
265+
ConfigurationProperties annotation = AnnotationUtils
266+
.findAnnotation(bean.getClass(), ConfigurationProperties.class);
251267
if (annotation != null || bean instanceof ConfigurationPropertiesHolder) {
252268
postProcessBeforeInitialization(bean, beanName, annotation);
253269
}
@@ -267,29 +283,29 @@ public Object postProcessAfterInitialization(Object bean, String beanName)
267283

268284
private void postProcessBeforeInitialization(Object bean, String beanName,
269285
ConfigurationProperties annotation) {
270-
Object target = (bean instanceof ConfigurationPropertiesHolder ? ((ConfigurationPropertiesHolder) bean)
271-
.getTarget() : bean);
286+
Object target = (bean instanceof ConfigurationPropertiesHolder
287+
? ((ConfigurationPropertiesHolder) bean).getTarget() : bean);
272288
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
273289
target);
274290
if (annotation != null && annotation.locations().length != 0) {
275-
factory.setPropertySources(loadPropertySources(annotation.locations(),
276-
annotation.merge()));
291+
factory.setPropertySources(
292+
loadPropertySources(annotation.locations(), annotation.merge()));
277293
}
278294
else {
279295
factory.setPropertySources(this.propertySources);
280296
}
281297
factory.setValidator(determineValidator(bean));
282298
// If no explicit conversion service is provided we add one so that (at least)
283299
// comma-separated arrays of convertibles can be bound automatically
284-
factory.setConversionService(this.conversionService == null ? getDefaultConversionService()
285-
: this.conversionService);
300+
factory.setConversionService(this.conversionService == null
301+
? getDefaultConversionService() : this.conversionService);
286302
if (annotation != null) {
287303
factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
288304
factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
289305
factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
290306
factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
291-
String targetName = (StringUtils.hasLength(annotation.value()) ? annotation
292-
.value() : annotation.prefix());
307+
String targetName = (StringUtils.hasLength(annotation.value())
308+
? annotation.value() : annotation.prefix());
293309
if (StringUtils.hasLength(targetName)) {
294310
factory.setTargetName(targetName);
295311
}
@@ -300,7 +316,8 @@ private void postProcessBeforeInitialization(Object bean, String beanName,
300316
catch (Exception ex) {
301317
String targetClass = ClassUtils.getShortName(target.getClass());
302318
throw new BeanCreationException(beanName, "Could not bind properties to "
303-
+ targetClass + " (" + getAnnotationDetails(annotation) + ")", ex);
319+
+ targetClass + " (" + getAnnotationDetails(annotation) + ")",
320+
ex);
304321
}
305322
}
306323

@@ -309,19 +326,18 @@ private String getAnnotationDetails(ConfigurationProperties annotation) {
309326
return "";
310327
}
311328
StringBuilder details = new StringBuilder();
312-
details.append("prefix=").append(
313-
(StringUtils.hasLength(annotation.value()) ? annotation.value()
314-
: annotation.prefix()));
329+
details.append("prefix=").append((StringUtils.hasLength(annotation.value())
330+
? annotation.value() : annotation.prefix()));
315331
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
316332
details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields());
317-
details.append(", ignoreNestedProperties=").append(
318-
annotation.ignoreNestedProperties());
333+
details.append(", ignoreNestedProperties=")
334+
.append(annotation.ignoreNestedProperties());
319335
return details.toString();
320336
}
321337

322338
private Validator determineValidator(Object bean) {
323-
boolean globalValidatorSupportBean = (this.validator != null && this.validator
324-
.supports(bean.getClass()));
339+
boolean globalValidatorSupportBean = (this.validator != null
340+
&& this.validator.supports(bean.getClass()));
325341
if (ClassUtils.isAssignable(Validator.class, bean.getClass())) {
326342
if (!globalValidatorSupportBean) {
327343
return (Validator) bean;
@@ -336,8 +352,8 @@ private PropertySources loadPropertySources(String[] locations,
336352
try {
337353
PropertySourcesLoader loader = new PropertySourcesLoader();
338354
for (String location : locations) {
339-
Resource resource = this.resourceLoader.getResource(this.environment
340-
.resolvePlaceholders(location));
355+
Resource resource = this.resourceLoader
356+
.getResource(this.environment.resolvePlaceholders(location));
341357
String[] profiles = this.environment.getActiveProfiles();
342358
for (int i = profiles.length; i-- > 0;) {
343359
String profile = profiles[i];
@@ -361,8 +377,9 @@ private PropertySources loadPropertySources(String[] locations,
361377
private ConversionService getDefaultConversionService() {
362378
if (this.defaultConversionService == null) {
363379
DefaultConversionService conversionService = new DefaultConversionService();
364-
for (Converter<?, ?> converter : ((ListableBeanFactory) this.beanFactory)
365-
.getBeansOfType(Converter.class, false, false).values()) {
380+
this.applicationContext.getAutowireCapableBeanFactory()
381+
.autowireBean(this);
382+
for (Converter<?, ?> converter : this.converters) {
366383
conversionService.addConverter(converter);
367384
}
368385
this.defaultConversionService = conversionService;
@@ -371,8 +388,8 @@ private ConversionService getDefaultConversionService() {
371388
}
372389

373390
/**
374-
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent class
375-
* loader issues.
391+
* Factory to create JSR 303 LocalValidatorFactoryBean. Inner class to prevent
392+
* class loader issues.
376393
*/
377394
private static class Jsr303ValidatorFactory {
378395

@@ -386,8 +403,8 @@ public Validator run(ApplicationContext applicationContext) {
386403
}
387404

388405
/**
389-
* {@link Validator} implementation that wraps {@link Validator} instances and chains
390-
* their execution.
406+
* {@link Validator} implementation that wraps {@link Validator} instances and
407+
* chains their execution.
391408
*/
392409
private static class ChainingValidator implements Validator {
393410

@@ -421,7 +438,8 @@ public void validate(Object target, Errors errors) {
421438

422439
/**
423440
* Convenience class to flatten out a tree of property sources without losing the
424-
* reference to the backing data (which can therefore be updated in the background).
441+
* reference to the backing data (which can therefore be updated in the
442+
* background).
425443
*/
426444
private static class FlatPropertySources implements PropertySources {
427445

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2012-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.bind;
18+
19+
import static org.hamcrest.Matchers.is;
20+
import static org.junit.Assert.assertThat;
21+
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.beans.factory.annotation.Value;
26+
import org.springframework.boot.bind.ConverterBindingTests.TestConfig;
27+
import org.springframework.boot.context.properties.ConfigurationProperties;
28+
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
29+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
30+
import org.springframework.boot.test.IntegrationTest;
31+
import org.springframework.boot.test.SpringApplicationConfiguration;
32+
import org.springframework.context.annotation.Bean;
33+
import org.springframework.context.annotation.Configuration;
34+
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
35+
import org.springframework.core.convert.converter.Converter;
36+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
37+
38+
/**
39+
* Tests for {@link ConfigurationProperties} binding with custom converters.
40+
*
41+
* @author Dave Syer
42+
*/
43+
@RunWith(SpringJUnit4ClassRunner.class)
44+
@SpringApplicationConfiguration(TestConfig.class)
45+
@IntegrationTest("foo=bar")
46+
public class ConverterBindingTests {
47+
48+
@Value("${foo:}")
49+
private String foo;
50+
51+
@Autowired
52+
private Wrapper properties;
53+
54+
@Test
55+
public void overridingOfPropertiesOrderOfAtPropertySources() {
56+
assertThat(this.properties.getFoo().getName(), is(this.foo));
57+
}
58+
59+
@Configuration
60+
@EnableConfigurationProperties(Wrapper.class)
61+
public static class TestConfig {
62+
63+
@Bean
64+
@ConfigurationPropertiesBinding
65+
public Converter<String, Foo> converter() {
66+
return new Converter<String, ConverterBindingTests.Foo>() {
67+
68+
@Override
69+
public Foo convert(String source) {
70+
Foo foo = new Foo();
71+
foo.setName(source);
72+
return foo;
73+
}
74+
};
75+
}
76+
77+
@Bean
78+
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
79+
return new PropertySourcesPlaceholderConfigurer();
80+
}
81+
82+
}
83+
84+
public static class Foo {
85+
private String name;
86+
87+
public String getName() {
88+
return this.name;
89+
}
90+
91+
public void setName(String name) {
92+
this.name = name;
93+
}
94+
95+
}
96+
97+
@ConfigurationProperties
98+
public static class Wrapper {
99+
private Foo foo;
100+
101+
public Foo getFoo() {
102+
return this.foo;
103+
}
104+
105+
public void setFoo(Foo foo) {
106+
this.foo = foo;
107+
}
108+
}
109+
110+
}

0 commit comments

Comments
 (0)