From 0d27ce4850a71637d0f006499fca21d4a008dcfb Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 12 May 2014 15:55:46 +0100 Subject: [PATCH] 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. --- .../context/properties/BeanMetaDataStore.java | 98 +++++++++++++++++++ .../properties/ConfigurationProperties.java | 2 +- ...urationPropertiesBindingPostProcessor.java | 27 ++++- ...opertiesBindingPostProcessorRegistrar.java | 16 +-- .../EnableConfigurationPropertiesTests.java | 21 ++++ 5 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/BeanMetaDataStore.java diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/BeanMetaDataStore.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/BeanMetaDataStore.java new file mode 100644 index 0000000000..c7e844097f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/BeanMetaDataStore.java @@ -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 @Bean definition meta data during initialization + * of the bean factory. + * + * @author Dave Syer + */ +class BeanMetaDataStore implements BeanFactoryPostProcessor { + + private ConfigurableListableBeanFactory beanFactory; + + private Map beans = new HashMap(); + + @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 found = new AtomicReference(); + 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; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java index 7a14cdf651..2f62a630b5 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java @@ -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 { 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 6ba45f10fb..ada35b1dcb 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,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); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java index b84b1f5100..3ebd7616e3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java @@ -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()); } } - } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java index f8a8d10dc6..8002c0b79a 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java @@ -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.