Enable @ConfigurationProperties on a @Bean definition

This allows multiple beans of the same type to be bound with
different prefixes. As a side effect you get default binding
if the bean class is itself @ConfigurationProperties.
This commit is contained in:
Dave Syer
2014-05-12 15:55:46 +01:00
parent 1b284a0619
commit 0d27ce4850
5 changed files with 156 additions and 8 deletions

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012-2013 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.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
/**
* Utility class to memoize <code>@Bean</code> definition meta data during initialization
* of the bean factory.
*
* @author Dave Syer
*/
class BeanMetaDataStore implements BeanFactoryPostProcessor {
private ConfigurableListableBeanFactory beanFactory;
private Map<String, MetaData> beans = new HashMap<String, MetaData>();
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
this.beanFactory = beanFactory;
for (String name : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(name);
String method = definition.getFactoryMethodName();
String bean = definition.getFactoryBeanName();
if (method != null && bean != null) {
this.beans.put(name, new MetaData(bean, method));
}
}
}
public Method findFactoryMethod(String beanName) {
if (!this.beans.containsKey(beanName)) {
return null;
}
final AtomicReference<Method> found = new AtomicReference<Method>();
MetaData meta = this.beans.get(beanName);
final String factory = meta.getMethod();
Class<?> type = this.beanFactory.getType(meta.getBean());
ReflectionUtils.doWithMethods(type, new MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException,
IllegalAccessException {
if (method.getName().equals(factory)) {
found.set(method);
}
}
});
return found.get();
}
private static class MetaData {
private String bean;
private String method;
public MetaData(String bean, String method) {
this.bean = bean;
this.method = method;
}
public String getBean() {
return this.bean;
}
public String getMethod() {
return this.method;
}
}
}

View File

@@ -29,7 +29,7 @@ import java.lang.annotation.Target;
* @author Dave Syer
* @see ConfigurationPropertiesBindingPostProcessor
*/
@Target(ElementType.TYPE)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.context.properties;
import java.io.IOException;
import java.lang.reflect.Method;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
@@ -74,6 +75,8 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
private static final String[] VALIDATOR_CLASSES = { "javax.validation.Validator",
"javax.validation.ValidatorFactory" };
private BeanMetaDataStore beans = new BeanMetaDataStore();
private PropertySources propertySources;
private Validator validator;
@@ -132,6 +135,13 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
this.conversionService = conversionService;
}
/**
* @param beans the bean meta data to set
*/
public void setBeanMetaDataStore(BeanMetaDataStore beans) {
this.beans = beans;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
@@ -277,9 +287,24 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
if (annotation != null || bean instanceof ConfigurationPropertiesHolder) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
annotation = maybePostProcessAnnotatedFactoryMethod(bean, beanName);
if (annotation != null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
return bean;
}
private ConfigurationProperties maybePostProcessAnnotatedFactoryMethod(Object bean,
String beanName) {
Method method = this.beans.findFactoryMethod(beanName);
if (method != null) {
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(method,
ConfigurationProperties.class);
return annotation;
}
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
@@ -353,7 +378,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
}
private ConversionService getDefaultConversionService() {
if (!this.initialized && this.beanFactory instanceof ListableBeanFactory) {
if (!this.initialized) {
for (Converter<?, ?> converter : ((ListableBeanFactory) this.beanFactory)
.getBeansOfType(Converter.class).values()) {
this.defaultConversionService.addConverter(converter);

View File

@@ -16,9 +16,8 @@
package org.springframework.boot.context.properties;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
@@ -35,14 +34,19 @@ public class ConfigurationPropertiesBindingPostProcessorRegistrar implements
public static final String BINDER_BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class
.getName();
private static final String METADATA_BEAN_NAME = BINDER_BEAN_NAME + ".store";
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(BINDER_BEAN_NAME)) {
BeanDefinition beanDefinition = new RootBeanDefinition(
ConfigurationPropertiesBindingPostProcessor.class);
registry.registerBeanDefinition(BINDER_BEAN_NAME, beanDefinition);
BeanDefinitionBuilder meta = BeanDefinitionBuilder
.genericBeanDefinition(BeanMetaDataStore.class);
BeanDefinitionBuilder bean = BeanDefinitionBuilder
.genericBeanDefinition(ConfigurationPropertiesBindingPostProcessor.class);
bean.addPropertyReference("beanMetaDataStore", METADATA_BEAN_NAME);
registry.registerBeanDefinition(BINDER_BEAN_NAME, bean.getBeanDefinition());
registry.registerBeanDefinition(METADATA_BEAN_NAME, meta.getBeanDefinition());
}
}
}

View File

@@ -370,6 +370,27 @@ public class EnableConfigurationPropertiesTests {
assertEquals("value12", bean.mymap.get("key1.key2"));
}
@Test
public void testAnnotatedBean() {
EnvironmentTestUtils.addEnvironment(this.context, "external.name:bar",
"spam.name:foo");
this.context.register(TestConfigurationWithAnnotatedBean.class);
this.context.refresh();
assertEquals("foo", this.context.getBean(External.class).getName());
}
@Configuration
@EnableConfigurationProperties
public static class TestConfigurationWithAnnotatedBean {
@Bean
@ConfigurationProperties(prefix = "spam")
public External testProperties() {
return new External();
}
}
/**
* Strict tests need a known set of properties so we remove system items which may be
* environment specific.