From 85719f75e4c18c30118884188935d1fac00ca929 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 12 May 2014 16:47:30 +0100 Subject: [PATCH] Add @Bean-level config properties to /configprops --- .../EndpointAutoConfiguration.java | 12 +- ...ConfigurationPropertiesReportEndpoint.java | 32 +++- ...sReportEndpointMethodAnnotationsTests.java | 159 ++++++++++++++++++ ... => ConfigurationBeanFactoryMetaData.java} | 25 ++- ...urationPropertiesBindingPostProcessor.java | 19 +-- ...opertiesBindingPostProcessorRegistrar.java | 2 +- 6 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java rename spring-boot/src/main/java/org/springframework/boot/context/properties/{BeanMetaDataStore.java => ConfigurationBeanFactoryMetaData.java} (77%) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java index d4a9ce7a9a..92a33764e1 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.bind.PropertiesConfigurationFactory; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; @@ -71,13 +72,16 @@ public class EndpointAutoConfiguration { private InfoPropertiesConfiguration properties; @Autowired(required = false) - private final MetricReader metricRepository = new InMemoryMetricRepository(); + private MetricReader metricRepository = new InMemoryMetricRepository(); @Autowired(required = false) private PublicMetrics metrics; @Autowired(required = false) - private final TraceRepository traceRepository = new InMemoryTraceRepository(); + private TraceRepository traceRepository = new InMemoryTraceRepository(); + + @Autowired(required = false) + private ConfigurationBeanFactoryMetaData beanFactoryMetaData; @Bean @ConditionalOnMissingBean @@ -159,7 +163,9 @@ public class EndpointAutoConfiguration { @Bean @ConditionalOnMissingBean public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint() { - return new ConfigurationPropertiesReportEndpoint(); + ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(); + endpoint.setConfigurationBeanFactoryMetaData(this.beanFactoryMetaData); + return endpoint; } @Configuration diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java index 9a095fb43b..46b4f2a9f7 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; import org.springframework.beans.BeansException; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -47,6 +48,7 @@ import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; * in your Spring Boot application configuration. * * @author Christian Dupuis + * @author Dave Syer */ @ConfigurationProperties(prefix = "endpoints.configprops", ignoreUnknownFields = false) public class ConfigurationPropertiesReportEndpoint extends @@ -58,6 +60,8 @@ public class ConfigurationPropertiesReportEndpoint extends private ApplicationContext context; + private ConfigurationBeanFactoryMetaData beanFactoryMetaData; + public ConfigurationPropertiesReportEndpoint() { super("configprops"); } @@ -71,6 +75,11 @@ public class ConfigurationPropertiesReportEndpoint extends this.context = context; } + public void setConfigurationBeanFactoryMetaData( + ConfigurationBeanFactoryMetaData beanFactoryMetaData) { + this.beanFactoryMetaData = beanFactoryMetaData; + } + public void setKeysToSanitize(String... keysToSanitize) { Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); this.keysToSanitize = keysToSanitize; @@ -87,9 +96,14 @@ public class ConfigurationPropertiesReportEndpoint extends */ @SuppressWarnings("unchecked") protected Map extract(ApplicationContext context) { + Map result = new HashMap(); - Map beans = context - .getBeansWithAnnotation(ConfigurationProperties.class); + Map beans = new HashMap( + context.getBeansWithAnnotation(ConfigurationProperties.class)); + if (this.beanFactoryMetaData != null) { + beans.putAll(this.beanFactoryMetaData + .getBeansWithFactoryAnnotation(ConfigurationProperties.class)); + } // Serialize beans into map structure and sanitize values ObjectMapper mapper = new ObjectMapper(); @@ -100,7 +114,7 @@ public class ConfigurationPropertiesReportEndpoint extends Object bean = entry.getValue(); Map root = new HashMap(); - root.put("prefix", extractPrefix(bean)); + root.put("prefix", extractPrefix(beanName, bean)); root.put("properties", sanitize(mapper.convertValue(bean, Map.class))); result.put(beanName, root); } @@ -134,9 +148,19 @@ public class ConfigurationPropertiesReportEndpoint extends /** * Extract configuration prefix from {@link ConfigurationProperties} annotation. */ - private String extractPrefix(Object bean) { + private String extractPrefix(String beanName, Object bean) { ConfigurationProperties annotation = AnnotationUtils.findAnnotation( bean.getClass(), ConfigurationProperties.class); + if (this.beanFactoryMetaData != null) { + ConfigurationProperties override = this.beanFactoryMetaData + .findFactoryAnnotation(beanName, ConfigurationProperties.class); + if (override != null) { + // The @Bean-level @ConfigurationProperties overrides the one at type + // level when binding. Arguably we should render them both, but this one + // might be the most relevant for a starting point. + annotation = override; + } + } return (StringUtils.hasLength(annotation.value()) ? annotation.value() : annotation.prefix()); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java new file mode 100644 index 0000000000..2feb2d8f6d --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2013-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.actuate.endpoint; + +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ConfigurationPropertiesReportEndpointMethodAnnotationsTests { + + private AnnotationConfigApplicationContext context; + + @Before + public void setup() { + this.context = new AnnotationConfigApplicationContext(); + } + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testNaming() throws Exception { + this.context.register(Config.class); + EnvironmentTestUtils.addEnvironment(this.context, "other.name:foo", + "first.name:bar"); + this.context.refresh(); + ConfigurationPropertiesReportEndpoint report = this.context + .getBean(ConfigurationPropertiesReportEndpoint.class); + Map properties = report.invoke(); + Map nestedProperties = (Map) properties + .get("other"); + assertNotNull(nestedProperties); + assertEquals("other", nestedProperties.get("prefix")); + assertNotNull(nestedProperties.get("properties")); + } + + @Test + @SuppressWarnings("unchecked") + public void testOverride() throws Exception { + this.context.register(Other.class); + EnvironmentTestUtils.addEnvironment(this.context, "other.name:foo"); + this.context.refresh(); + ConfigurationPropertiesReportEndpoint report = this.context + .getBean(ConfigurationPropertiesReportEndpoint.class); + Map properties = report.invoke(); + Map nestedProperties = (Map) properties + .get("bar"); + assertNotNull(nestedProperties); + assertEquals("other", nestedProperties.get("prefix")); + assertNotNull(nestedProperties.get("properties")); + } + + @Configuration + @EnableConfigurationProperties + public static class Config { + + @Bean + public ConfigurationPropertiesReportEndpoint endpoint( + ConfigurationBeanFactoryMetaData beanFactoryMetaData) { + ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(); + endpoint.setConfigurationBeanFactoryMetaData(beanFactoryMetaData); + return endpoint; + } + + @Bean + @ConfigurationProperties(prefix = "first") + public Foo foo() { + return new Foo(); + } + + @Bean + @ConfigurationProperties(prefix = "other") + public Foo other() { + return new Foo(); + } + + } + + @Configuration + @EnableConfigurationProperties + public static class Other { + + @Bean + public ConfigurationPropertiesReportEndpoint endpoint( + ConfigurationBeanFactoryMetaData beanFactoryMetaData) { + ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(); + endpoint.setConfigurationBeanFactoryMetaData(beanFactoryMetaData); + return endpoint; + } + + @Bean + @ConfigurationProperties(prefix = "other") + public Bar bar() { + return new Bar(); + } + + } + + public static class Foo { + + private String name = "654321"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + @ConfigurationProperties(prefix = "test") + public static class Bar { + + private String name = "654321"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} 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/ConfigurationBeanFactoryMetaData.java similarity index 77% rename from spring-boot/src/main/java/org/springframework/boot/context/properties/BeanMetaDataStore.java rename to spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetaData.java index c7e844097f..2384debba3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/BeanMetaDataStore.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetaData.java @@ -16,6 +16,7 @@ package org.springframework.boot.context.properties; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -25,6 +26,7 @@ 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.core.annotation.AnnotationUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; @@ -34,7 +36,7 @@ import org.springframework.util.ReflectionUtils.MethodCallback; * * @author Dave Syer */ -class BeanMetaDataStore implements BeanFactoryPostProcessor { +public class ConfigurationBeanFactoryMetaData implements BeanFactoryPostProcessor { private ConfigurableListableBeanFactory beanFactory; @@ -54,7 +56,26 @@ class BeanMetaDataStore implements BeanFactoryPostProcessor { } } - public Method findFactoryMethod(String beanName) { + public Map getBeansWithFactoryAnnotation( + Class type) { + Map result = new HashMap(); + for (String name : this.beans.keySet()) { + if (findFactoryAnnotation(name, type) != null) { + result.put(name, this.beanFactory.getBean(name)); + } + } + return result; + } + + public A findFactoryAnnotation(String beanName, Class type) { + Method method = findFactoryMethod(beanName); + if (method != null) { + return AnnotationUtils.findAnnotation(method, type); + } + return null; + } + + private Method findFactoryMethod(String beanName) { if (!this.beans.containsKey(beanName)) { return null; } 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 ada35b1dcb..b4aae0c4af 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,6 @@ 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; @@ -75,7 +74,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc private static final String[] VALIDATOR_CLASSES = { "javax.validation.Validator", "javax.validation.ValidatorFactory" }; - private BeanMetaDataStore beans = new BeanMetaDataStore(); + private ConfigurationBeanFactoryMetaData beans = new ConfigurationBeanFactoryMetaData(); private PropertySources propertySources; @@ -138,7 +137,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc /** * @param beans the bean meta data to set */ - public void setBeanMetaDataStore(BeanMetaDataStore beans) { + public void setBeanMetaDataStore(ConfigurationBeanFactoryMetaData beans) { this.beans = beans; } @@ -287,24 +286,14 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc if (annotation != null || bean instanceof ConfigurationPropertiesHolder) { postProcessBeforeInitialization(bean, beanName, annotation); } - annotation = maybePostProcessAnnotatedFactoryMethod(bean, beanName); + annotation = this.beans.findFactoryAnnotation(beanName, + ConfigurationProperties.class); 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 { 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 3ebd7616e3..89b7b69323 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 @@ -41,7 +41,7 @@ public class ConfigurationPropertiesBindingPostProcessorRegistrar implements BeanDefinitionRegistry registry) { if (!registry.containsBeanDefinition(BINDER_BEAN_NAME)) { BeanDefinitionBuilder meta = BeanDefinitionBuilder - .genericBeanDefinition(BeanMetaDataStore.class); + .genericBeanDefinition(ConfigurationBeanFactoryMetaData.class); BeanDefinitionBuilder bean = BeanDefinitionBuilder .genericBeanDefinition(ConfigurationPropertiesBindingPostProcessor.class); bean.addPropertyReference("beanMetaDataStore", METADATA_BEAN_NAME);