From 221ff0ff6de8bd9557f3fb6d7b336a188b2dd224 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Sep 2019 12:34:11 -0700 Subject: [PATCH] Polish configuration property bean classes Refactor the `org.springframework.boot.actuate.context` package with the following changes: - Deprecate several classes which would ideally be internal - Replace `ConfigurationBeanFactoryMetadata` with a new `ConfigurationPropertiesBean` class to better reflect that we no longer maintain meta-data directly. - Use constructor injection and final fields whenever possible - Rename `ConfiguraionPropertiesBeanDefinition` to `ConfigurationPropertiesValueObjectBeanDefinition` to align with the binder changes made in commit 0b3015e4ff - Add additional tests Closes gh-16903 --- ...ConfigurationPropertiesReportEndpoint.java | 57 +--- .../ConfigurationBeanFactoryMetadata.java | 15 +- .../ConfigurationPropertiesBean.java | 205 +++++++++++++ ...ConfigurationPropertiesBeanDefinition.java | 70 ----- ...tionPropertiesBeanDefinitionRegistrar.java | 112 -------- .../ConfigurationPropertiesBeanRegistrar.java | 108 +++++-- .../ConfigurationPropertiesBindException.java | 4 + .../ConfigurationPropertiesBinder.java | 87 ++++-- ...urationPropertiesBindingPostProcessor.java | 99 +++---- ...opertiesBindingPostProcessorRegistrar.java | 43 +-- .../ConfigurationPropertiesScan.java | 7 +- .../ConfigurationPropertiesScanRegistrar.java | 75 +++-- ...onPropertiesValueObjectBeanDefinition.java | 67 +++++ .../EnableConfigurationProperties.java | 8 +- ...nableConfigurationPropertiesRegistrar.java | 55 ++++ .../properties/PropertySourcesDeducer.java | 23 +- ...igurationPropertiesBeanRegistrarTests.java | 118 +++----- .../ConfigurationPropertiesBeanTests.java | 272 ++++++++++++++++++ ...igurationPropertiesBindExceptionTests.java | 73 +++++ ...igurationPropertiesScanRegistrarTests.java | 13 +- .../ConfigurationPropertiesTests.java | 9 +- .../ConversionServiceDeducerTests.java | 106 +++++++ ...ConfigurationPropertiesRegistrarTests.java | 150 ++++++++++ .../PropertySourcesDeducerTests.java | 128 +++++++++ ...nfigurationPropertiesBeanRegistrarTests.kt | 25 +- 25 files changed, 1395 insertions(+), 534 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionRegistrar.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindExceptionTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertySourcesDeducerTests.java diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index 0a70be4447..1441bd7bb7 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -49,8 +49,8 @@ import org.springframework.beans.BeansException; import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.ClassUtils; @@ -108,36 +108,17 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext private ContextConfigurationProperties describeConfigurationProperties(ApplicationContext context, ObjectMapper mapper) { - ConfigurationBeanFactoryMetadata beanFactoryMetadata = getBeanFactoryMetadata(context); - Map beans = getConfigurationPropertiesBeans(context, beanFactoryMetadata); - Map beanDescriptors = new HashMap<>(); + Map beans = ConfigurationPropertiesBean.getAll(context); + Map descriptors = new HashMap<>(); beans.forEach((beanName, bean) -> { - String prefix = extractPrefix(context, beanFactoryMetadata, beanName); - beanDescriptors.put(beanName, new ConfigurationPropertiesBeanDescriptor(prefix, - sanitize(prefix, safeSerialize(mapper, bean, prefix)))); + String prefix = bean.getAnnotation().prefix(); + descriptors.put(beanName, new ConfigurationPropertiesBeanDescriptor(prefix, + sanitize(prefix, safeSerialize(mapper, bean.getInstance(), prefix)))); }); - return new ContextConfigurationProperties(beanDescriptors, + return new ContextConfigurationProperties(descriptors, (context.getParent() != null) ? context.getParent().getId() : null); } - private ConfigurationBeanFactoryMetadata getBeanFactoryMetadata(ApplicationContext context) { - Map beans = context - .getBeansOfType(ConfigurationBeanFactoryMetadata.class); - if (beans.size() == 1) { - return beans.values().iterator().next(); - } - return null; - } - - private Map getConfigurationPropertiesBeans(ApplicationContext context, - ConfigurationBeanFactoryMetadata beanFactoryMetadata) { - Map beans = new HashMap<>(context.getBeansWithAnnotation(ConfigurationProperties.class)); - if (beanFactoryMetadata != null) { - beans.putAll(beanFactoryMetadata.getBeansWithFactoryAnnotation(ConfigurationProperties.class)); - } - return beans; - } - /** * Cautiously serialize the bean to a map (returning a map with an error message * instead of throwing an exception if there is a problem). @@ -197,30 +178,6 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter())); } - /** - * Extract configuration prefix from - * {@link ConfigurationProperties @ConfigurationProperties} annotation. - * @param context the application context - * @param beanFactoryMetaData the bean factory meta-data - * @param beanName the bean name - * @return the prefix - */ - private String extractPrefix(ApplicationContext context, ConfigurationBeanFactoryMetadata beanFactoryMetaData, - String beanName) { - ConfigurationProperties annotation = context.findAnnotationOnBean(beanName, ConfigurationProperties.class); - if (beanFactoryMetaData != null) { - ConfigurationProperties override = 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 annotation.prefix(); - } - /** * Sanitize all unwanted configuration properties to avoid leaking of sensitive * information. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java index 25710ccdab..15df03eb30 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java @@ -24,6 +24,8 @@ import java.util.Map; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -31,12 +33,14 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotationUtils; /** - * Utility class to memorize {@code @Bean} definition meta data during initialization of + * Utility class to memorize {@code @Bean} definition metadata during initialization of * the bean factory. * * @author Dave Syer * @since 1.1.0 + * @deprecated since 2.2.0 in favor of {@link ConfigurationPropertiesBean} */ +@Deprecated public class ConfigurationBeanFactoryMetadata implements ApplicationContextAware { /** @@ -77,4 +81,13 @@ public class ConfigurationBeanFactoryMetadata implements ApplicationContextAware this.applicationContext = (ConfigurableApplicationContext) applicationContext; } + static void register(BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(BEAN_NAME)) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationBeanFactoryMetadata.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME, definition); + } + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java new file mode 100644 index 0000000000..cf0cca0570 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java @@ -0,0 +1,205 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.Annotation; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +/** + * Provides access to {@link ConfigurationProperties} beans from an + * {@link ApplicationContext}. + * + * @author Phillip Webb + * @since 2.0.0 + * @see #get(ApplicationContext, Object, String) + * @see #getAll(ApplicationContext) + */ +public final class ConfigurationPropertiesBean { + + private final String name; + + private final Object instance; + + private final ConfigurationProperties annotation; + + private final Bindable bindTarget; + + private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation, + Bindable bindTarget) { + this.name = name; + this.instance = instance; + this.annotation = annotation; + this.bindTarget = bindTarget; + } + + /** + * Return the name of the Spring bean. + * @return the bean name + */ + public String getName() { + return this.name; + } + + /** + * Return the actual Spring bean instance. + * @return the bean instance + */ + public Object getInstance() { + return this.instance; + } + + /** + * Return the {@link ConfigurationProperties} annotation for the bean. The annotation + * may be defined on the bean itself or from the factory method that create the bean + * (usually a {@link Bean @Bean} method). + * @return the configuration properties annotation + */ + public ConfigurationProperties getAnnotation() { + return this.annotation; + } + + /** + * Return a {@link Bindable} instance suitable that can be used as a target for the + * {@link Binder}. + * @return a bind target for use with the {@link Binder} + */ + public Bindable asBindTarget() { + return this.bindTarget; + } + + /** + * Return all {@link ConfigurationProperties @ConfigurationProperties} beans contained + * in the given application context. Both directly annotated beans, as well as beans + * that have {@link ConfigurationProperties @ConfigurationProperties} annotated + * factory methods are included. + * @param applicationContext the source application context + * @return a map of all configuration properties beans keyed by the bean name + */ + public static Map getAll(ApplicationContext applicationContext) { + Assert.notNull(applicationContext, "ApplicationContext must not be null"); + if (applicationContext instanceof ConfigurableApplicationContext) { + return getAll((ConfigurableApplicationContext) applicationContext); + } + Map propertiesBeans = new LinkedHashMap<>(); + applicationContext.getBeansWithAnnotation(ConfigurationProperties.class) + .forEach((beanName, bean) -> propertiesBeans.put(beanName, get(applicationContext, bean, beanName))); + return propertiesBeans; + } + + private static Map getAll(ConfigurableApplicationContext applicationContext) { + Map propertiesBeans = new LinkedHashMap<>(); + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + Iterator beanNames = beanFactory.getBeanNamesIterator(); + while (beanNames.hasNext()) { + String beanName = beanNames.next(); + try { + Object bean = beanFactory.getBean(beanName); + ConfigurationPropertiesBean propertiesBean = get(applicationContext, bean, beanName); + if (propertiesBean != null) { + propertiesBeans.put(beanName, propertiesBean); + } + } + catch (NoSuchBeanDefinitionException ex) { + } + } + return propertiesBeans; + } + + /** + * Return a {@link ConfigurationPropertiesBean @ConfigurationPropertiesBean} instance + * for the given bean details or {@code null} if the bean is not a + * {@link ConfigurationProperties @ConfigurationProperties} object. Annotations are + * considered both on the bean itself, as well as any factory method (for example a + * {@link Bean @Bean} method). + * @param applicationContext the source application context + * @param bean the bean to consider + * @param beanName the bean name + * @return a configuration properties bean or {@code null} if the neither the bean or + * factory method are annotated with + * {@link ConfigurationProperties @ConfigurationProperties} + */ + public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) { + Method factoryMethod = findFactoryMethod(applicationContext, beanName); + ConfigurationProperties annotation = getAnnotation(applicationContext, bean, beanName, factoryMethod, + ConfigurationProperties.class); + if (annotation == null) { + return null; + } + ResolvableType type = (factoryMethod != null) ? ResolvableType.forMethodReturnType(factoryMethod) + : ResolvableType.forClass(bean.getClass()); + Validated validated = getAnnotation(applicationContext, bean, beanName, factoryMethod, Validated.class); + Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated } + : new Annotation[] { annotation }; + Bindable bindTarget = Bindable.of(type).withAnnotations(annotations).withExistingValue(bean); + return new ConfigurationPropertiesBean(beanName, bean, annotation, bindTarget); + } + + private static Method findFactoryMethod(ApplicationContext applicationContext, String beanName) { + if (applicationContext instanceof ConfigurableApplicationContext) { + return findFactoryMethod((ConfigurableApplicationContext) applicationContext, beanName); + } + return null; + } + + private static Method findFactoryMethod(ConfigurableApplicationContext applicationContext, String beanName) { + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + if (beanFactory.containsBeanDefinition(beanName)) { + BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); + if (beanDefinition instanceof RootBeanDefinition) { + return ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod(); + } + } + return null; + } + + private static A getAnnotation(ApplicationContext applicationContext, Object bean, + String beanName, Method factoryMethod, Class annotationType) { + if (factoryMethod != null) { + A annotation = AnnotationUtils.findAnnotation(factoryMethod, annotationType); + if (annotation != null) { + return annotation; + } + } + A annotation = AnnotationUtils.findAnnotation(bean.getClass(), annotationType); + if (annotation != null) { + return annotation; + } + if (AopUtils.isAopProxy(bean)) { + return AnnotationUtils.findAnnotation(AopUtils.getTargetClass(bean), annotationType); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java deleted file mode 100644 index b424e719e2..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-2019 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 - * - * https://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.Annotation; -import java.util.function.Supplier; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.validation.annotation.Validated; - -/** - * {@link BeanDefinition} that is used for registering - * {@link ConfigurationProperties @ConfigurationProperties} beans that are bound at - * creation time. - * - * @author Stephane Nicoll - * @author Madhura Bhave - */ -final class ConfigurationPropertiesBeanDefinition extends GenericBeanDefinition { - - static ConfigurationPropertiesBeanDefinition from(ConfigurableListableBeanFactory beanFactory, String beanName, - Class type) { - ConfigurationPropertiesBeanDefinition beanDefinition = new ConfigurationPropertiesBeanDefinition(); - beanDefinition.setBeanClass(type); - beanDefinition.setInstanceSupplier(createBean(beanFactory, beanName, type)); - return beanDefinition; - } - - private static Supplier createBean(ConfigurableListableBeanFactory beanFactory, String beanName, - Class type) { - return () -> { - ConfigurationProperties annotation = getAnnotation(type, ConfigurationProperties.class); - Validated validated = getAnnotation(type, Validated.class); - Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated } - : new Annotation[] { annotation }; - Bindable bindable = Bindable.of(type).withAnnotations(annotations); - ConfigurationPropertiesBinder binder = beanFactory.getBean(ConfigurationPropertiesBinder.BEAN_NAME, - ConfigurationPropertiesBinder.class); - try { - return binder.bindOrCreate(bindable); - } - catch (Exception ex) { - throw new ConfigurationPropertiesBindException(beanName, type, annotation, ex); - } - }; - } - - private static A getAnnotation(Class type, Class annotationType) { - return AnnotationUtils.findAnnotation(type, annotationType); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionRegistrar.java deleted file mode 100644 index fe02ecbe97..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionRegistrar.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2019 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 - * - * https://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.Constructor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.core.KotlinDetector; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Registers a bean definition for a type annotated with - * {@link ConfigurationProperties @ConfigurationProperties} using the prefix of the - * annotation in the bean name. - * - * @author Madhura Bhave - */ -final class ConfigurationPropertiesBeanDefinitionRegistrar { - - private static final boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent(); - - private ConfigurationPropertiesBeanDefinitionRegistrar() { - } - - static void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory, Class type) { - MergedAnnotation annotation = MergedAnnotations - .from(type, SearchStrategy.TYPE_HIERARCHY).get(ConfigurationProperties.class); - String name = getName(type, annotation); - if (!containsBeanDefinition(beanFactory, name)) { - registerBeanDefinition(registry, beanFactory, name, type, annotation); - } - } - - private static String getName(Class type, MergedAnnotation annotation) { - String prefix = annotation.isPresent() ? annotation.getString("prefix") : ""; - return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName()); - } - - private static boolean containsBeanDefinition(ConfigurableListableBeanFactory beanFactory, String name) { - if (beanFactory.containsBeanDefinition(name)) { - return true; - } - BeanFactory parent = beanFactory.getParentBeanFactory(); - if (parent instanceof ConfigurableListableBeanFactory) { - return containsBeanDefinition((ConfigurableListableBeanFactory) parent, name); - } - return false; - } - - private static void registerBeanDefinition(BeanDefinitionRegistry registry, - ConfigurableListableBeanFactory beanFactory, String name, Class type, - MergedAnnotation annotation) { - Assert.isTrue(annotation.isPresent(), () -> "No " + ConfigurationProperties.class.getSimpleName() - + " annotation found on '" + type.getName() + "'."); - registry.registerBeanDefinition(name, createBeanDefinition(beanFactory, name, type)); - } - - private static BeanDefinition createBeanDefinition(ConfigurableListableBeanFactory beanFactory, String name, - Class type) { - if (canBindAtCreationTime(type)) { - return ConfigurationPropertiesBeanDefinition.from(beanFactory, name, type); - } - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(type); - return definition; - } - - private static boolean canBindAtCreationTime(Class type) { - List> constructors = determineConstructors(type); - return (constructors.size() == 1 && constructors.get(0).getParameterCount() > 0); - } - - private static List> determineConstructors(Class type) { - List> constructors = new ArrayList<>(); - if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) { - Constructor primaryConstructor = BeanUtils.findPrimaryConstructor(type); - if (primaryConstructor != null) { - constructors.add(primaryConstructor); - } - } - else { - constructors.addAll(Arrays.asList(type.getDeclaredConstructors())); - } - return constructors; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java index fef8c68136..f85c9dea7c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java @@ -13,45 +13,101 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.boot.context.properties; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; +import java.lang.reflect.Constructor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.HierarchicalBeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.util.MultiValueMap; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.KotlinDetector; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** - * {@link ImportBeanDefinitionRegistrar} for configuration properties support. + * Delegate used by {@link EnableConfigurationPropertiesRegistrar} and + * {@link ConfigurationPropertiesScanRegistrar} to register a bean definition for a + * {@link ConfigurationProperties @ConfigurationProperties} class. * - * @author Dave Syer - * @author Christian Dupuis - * @author Stephane Nicoll + * @author Madhura Bhave + * @author Phillip Webb */ -class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar { +final class ConfigurationPropertiesBeanRegistrar { - @Override - public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) registry; - getTypes(metadata).forEach( - (type) -> ConfigurationPropertiesBeanDefinitionRegistrar.register(registry, beanFactory, type)); + private final BeanDefinitionRegistry registry; + + private final BeanFactory beanFactory; + + ConfigurationPropertiesBeanRegistrar(BeanDefinitionRegistry registry) { + this.registry = registry; + this.beanFactory = (BeanFactory) this.registry; } - private List> getTypes(AnnotationMetadata metadata) { - MultiValueMap attributes = metadata - .getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(), false); - return collectClasses((attributes != null) ? attributes.get("value") : Collections.emptyList()); + void register(Class type) { + MergedAnnotation annotation = MergedAnnotations + .from(type, SearchStrategy.TYPE_HIERARCHY).get(ConfigurationProperties.class); + register(type, annotation); } - private List> collectClasses(List values) { - return values.stream().flatMap((value) -> Arrays.stream((Class[]) value)) - .filter((type) -> void.class != type).collect(Collectors.toList()); + void register(Class type, MergedAnnotation annotation) { + String name = getName(type, annotation); + if (!containsBeanDefinition(name)) { + registerBeanDefinition(name, type, annotation); + } + } + + private String getName(Class type, MergedAnnotation annotation) { + String prefix = annotation.isPresent() ? annotation.getString("prefix") : ""; + return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName()); + } + + private boolean containsBeanDefinition(String name) { + return containsBeanDefinition(this.beanFactory, name); + } + + private boolean containsBeanDefinition(BeanFactory beanFactory, String name) { + if (beanFactory instanceof ListableBeanFactory + && ((ListableBeanFactory) beanFactory).containsBeanDefinition(name)) { + return true; + } + if (beanFactory instanceof HierarchicalBeanFactory) { + return containsBeanDefinition(((HierarchicalBeanFactory) beanFactory).getParentBeanFactory(), name); + } + return false; + } + + private void registerBeanDefinition(String beanName, Class type, + MergedAnnotation annotation) { + Assert.state(annotation.isPresent(), () -> "No " + ConfigurationProperties.class.getSimpleName() + + " annotation found on '" + type.getName() + "'."); + this.registry.registerBeanDefinition(beanName, createBeanDefinition(beanName, type)); + } + + private BeanDefinition createBeanDefinition(String beanName, Class type) { + if (isValueObject(type)) { + return new ConfigurationPropertiesValueObjectBeanDefinition(this.beanFactory, beanName, type); + } + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(type); + return definition; + } + + private boolean isValueObject(Class type) { + if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) { + Constructor primaryConstructor = BeanUtils.findPrimaryConstructor(type); + if (primaryConstructor != null) { + return primaryConstructor.getParameterCount() > 0; + } + } + Constructor[] constructors = type.getDeclaredConstructors(); + return constructors.length == 1 && constructors[0].getParameterCount() > 0; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java index fa91e8d823..90eac42cc8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java @@ -33,6 +33,10 @@ public class ConfigurationPropertiesBindException extends BeanCreationException private final ConfigurationProperties annotation; + ConfigurationPropertiesBindException(ConfigurationPropertiesBean bean, Exception cause) { + this(bean.getName(), bean.getInstance().getClass(), bean.getAnnotation(), cause); + } + ConfigurationPropertiesBindException(String beanName, Class beanType, ConfigurationProperties annotation, Exception cause) { super(beanName, getMessage(beanType, annotation), cause); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index de306aeced..fb5a1c7348 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -23,6 +23,11 @@ import java.util.stream.Collectors; import org.springframework.beans.BeansException; import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.Bindable; @@ -45,43 +50,36 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; /** - * Internal class by the {@link ConfigurationPropertiesBindingPostProcessor} to handle the - * actual {@link ConfigurationProperties @ConfigurationProperties} binding. + * Internal class used by the {@link ConfigurationPropertiesBindingPostProcessor} to + * handle the actual {@link ConfigurationProperties @ConfigurationProperties} binding. * * @author Stephane Nicoll * @author Phillip Webb */ -class ConfigurationPropertiesBinder implements ApplicationContextAware { +class ConfigurationPropertiesBinder { - /** - * The bean name that this binder is registered with. - */ - static final String BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinder"; + private static final String BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinder"; - private final String validatorBeanName; + private static final String FACTORY_BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinderFactory"; - private ApplicationContext applicationContext; + private static final String VALIDATOR_BEAN_NAME = EnableConfigurationProperties.VALIDATOR_BEAN_NAME; - private PropertySources propertySources; + private final ApplicationContext applicationContext; - private Validator configurationPropertiesValidator; + private final PropertySources propertySources; - private boolean jsr303Present; + private final Validator configurationPropertiesValidator; + + private final boolean jsr303Present; private volatile Validator jsr303Validator; private volatile Binder binder; - ConfigurationPropertiesBinder(String validatorBeanName) { - this.validatorBeanName = validatorBeanName; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ConfigurationPropertiesBinder(ApplicationContext applicationContext) { this.applicationContext = applicationContext; this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources(); - this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext, - this.validatorBeanName); + this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext); this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext); } @@ -103,10 +101,9 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { return annotation; } - private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext, - String validatorBeanName) { - if (applicationContext.containsBean(validatorBeanName)) { - return applicationContext.getBean(validatorBeanName, Validator.class); + private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext) { + if (applicationContext.containsBean(VALIDATOR_BEAN_NAME)) { + return applicationContext.getBean(VALIDATOR_BEAN_NAME, Validator.class); } return null; } @@ -183,4 +180,46 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { return null; } + static void register(BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(FACTORY_BEAN_NAME)) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationPropertiesBinder.Factory.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(ConfigurationPropertiesBinder.FACTORY_BEAN_NAME, definition); + } + if (!registry.containsBeanDefinition(BEAN_NAME)) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationPropertiesBinder.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + definition.setFactoryBeanName(FACTORY_BEAN_NAME); + definition.setFactoryMethodName("create"); + registry.registerBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME, definition); + } + } + + static ConfigurationPropertiesBinder get(BeanFactory beanFactory) { + return beanFactory.getBean(BEAN_NAME, ConfigurationPropertiesBinder.class); + } + + /** + * Factory bean used to create the {@link ConfigurationPropertiesBinder}. The bean + * needs to be {@link ApplicationContextAware} since we can't directly inject an + * {@link ApplicationContext} into the constructor without causing eager + * {@link FactoryBean} initialization. + */ + static class Factory implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + ConfigurationPropertiesBinder create() { + return new ConfigurationPropertiesBinder(this.applicationContext); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index 879f0e0170..01a4ca37b9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -16,23 +16,18 @@ package org.springframework.boot.context.properties; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.PropertySources; -import org.springframework.validation.annotation.Validated; +import org.springframework.util.Assert; /** * {@link BeanPostProcessor} to bind {@link PropertySources} to beans annotated with @@ -56,16 +51,26 @@ public class ConfigurationPropertiesBindingPostProcessor /** * The bean name of the configuration properties validator. * @deprecated since 2.2.0 in favor of - * {@link ConfigurationPropertiesBindingPostProcessorRegistrar#VALIDATOR_BEAN_NAME} + * {@link EnableConfigurationProperties#VALIDATOR_BEAN_NAME} */ @Deprecated - public static final String VALIDATOR_BEAN_NAME = ConfigurationPropertiesBindingPostProcessorRegistrar.VALIDATOR_BEAN_NAME; - - private ConfigurationBeanFactoryMetadata beanFactoryMetadata; + public static final String VALIDATOR_BEAN_NAME = EnableConfigurationProperties.VALIDATOR_BEAN_NAME; private ApplicationContext applicationContext; - private ConfigurationPropertiesBinder configurationPropertiesBinder; + private BeanDefinitionRegistry registry; + + private ConfigurationPropertiesBinder binder; + + /** + * Create a new {@link ConfigurationPropertiesBindingPostProcessor} instance. + * @deprecated since 2.2.0 in favor of + * {@link EnableConfigurationProperties @EnableConfigurationProperties} or + * {@link ConfigurationPropertiesBindingPostProcessor#register(BeanDefinitionRegistry)} + */ + @Deprecated + public ConfigurationPropertiesBindingPostProcessor() { + } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -76,10 +81,8 @@ public class ConfigurationPropertiesBindingPostProcessor public void afterPropertiesSet() throws Exception { // We can't use constructor injection of the application context because // it causes eager factory bean initialization - this.beanFactoryMetadata = this.applicationContext.getBean(ConfigurationBeanFactoryMetadata.BEAN_NAME, - ConfigurationBeanFactoryMetadata.class); - this.configurationPropertiesBinder = this.applicationContext.getBean(ConfigurationPropertiesBinder.BEAN_NAME, - ConfigurationPropertiesBinder.class); + this.registry = (BeanDefinitionRegistry) this.applicationContext.getAutowireCapableBeanFactory(); + this.binder = ConfigurationPropertiesBinder.get(this.applicationContext); } @Override @@ -89,51 +92,39 @@ public class ConfigurationPropertiesBindingPostProcessor @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class); - if (annotation != null && !hasBeenBound(beanName)) { - bind(bean, beanName, annotation); + ConfigurationPropertiesBean configurationPropertiesBean = ConfigurationPropertiesBean + .get(this.applicationContext, bean, beanName); + if (configurationPropertiesBean != null && !hasBoundValueObject(beanName)) { + try { + this.binder.bind(configurationPropertiesBean.asBindTarget()); + } + catch (Exception ex) { + throw new ConfigurationPropertiesBindException(configurationPropertiesBean, ex); + } } return bean; } - private boolean hasBeenBound(String beanName) { - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) this.applicationContext - .getAutowireCapableBeanFactory(); - if (registry.containsBeanDefinition(beanName)) { - BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); - return beanDefinition instanceof ConfigurationPropertiesBeanDefinition; - } - return false; + private boolean hasBoundValueObject(String beanName) { + return this.registry.containsBeanDefinition(beanName) && this.registry + .getBeanDefinition(beanName) instanceof ConfigurationPropertiesValueObjectBeanDefinition; } - private void bind(Object bean, String beanName, ConfigurationProperties annotation) { - ResolvableType type = getBeanType(bean, beanName); - Validated validated = getAnnotation(bean, beanName, Validated.class); - Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated } - : new Annotation[] { annotation }; - Bindable target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations); - try { - this.configurationPropertiesBinder.bind(target); + /** + * Register a {@link ConfigurationPropertiesBindingPostProcessor} bean if one is not + * already registered. + * @param registry the bean definition registry + * @since 2.2.0 + */ + public static void register(BeanDefinitionRegistry registry) { + Assert.notNull(registry, "Registry must not be null"); + if (!registry.containsBeanDefinition(BEAN_NAME)) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(BEAN_NAME, definition); } - catch (Exception ex) { - throw new ConfigurationPropertiesBindException(beanName, bean.getClass(), annotation, ex); - } - } - - private ResolvableType getBeanType(Object bean, String beanName) { - Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName); - if (factoryMethod != null) { - return ResolvableType.forMethodReturnType(factoryMethod); - } - return ResolvableType.forClass(bean.getClass()); - } - - private A getAnnotation(Object bean, String beanName, Class type) { - A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type); - if (annotation == null) { - annotation = AnnotationUtils.findAnnotation(bean.getClass(), type); - } - return annotation; + ConfigurationPropertiesBinder.register(registry); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java index f58378ad16..5d4a596333 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java @@ -16,9 +16,7 @@ package org.springframework.boot.context.properties; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; @@ -29,45 +27,20 @@ import org.springframework.core.type.AnnotationMetadata; * @author Dave Syer * @author Phillip Webb * @since 1.0.0 + * @deprecated since 2.2.0 in favor of + * {@link EnableConfigurationProperties @EnableConfigurationProperties} */ +@Deprecated public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar { - /** - * The bean name of the configuration properties validator. - */ - public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; - @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - if (!registry.containsBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME)) { - registerConfigurationPropertiesBinder(registry); + // Spring Cloud Function may call this with a null importingClassMetadata + if (importingClassMetadata == null) { + EnableConfigurationPropertiesRegistrar.registerInfrastructureBeans(registry); + return; } - if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) { - registerConfigurationPropertiesBindingPostProcessor(registry); - registerConfigurationBeanFactoryMetadata(registry); - } - } - - private void registerConfigurationPropertiesBinder(BeanDefinitionRegistry registry) { - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(ConfigurationPropertiesBinder.class); - definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - definition.getConstructorArgumentValues().addIndexedArgumentValue(0, VALIDATOR_BEAN_NAME); - registry.registerBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME, definition); - } - - private void registerConfigurationPropertiesBindingPostProcessor(BeanDefinitionRegistry registry) { - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class); - definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition); - } - - private void registerConfigurationBeanFactoryMetadata(BeanDefinitionRegistry registry) { - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(ConfigurationBeanFactoryMetadata.class); - definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME, definition); + new EnableConfigurationPropertiesRegistrar().registerBeanDefinitions(importingClassMetadata, registry); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java index 6a002b71e1..41fe0bbd9c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java @@ -23,6 +23,7 @@ import java.lang.annotation.Target; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; /** * Configures the base packages used when scanning for @@ -30,6 +31,9 @@ import org.springframework.core.annotation.AliasFor; * {@link #basePackageClasses()}, {@link #basePackages()} or its alias {@link #value()} * may be specified to define specific packages to scan. If specific packages are not * defined scanning will occur from the package of the class with this annotation. + *

+ * Note: Classes annotated or meta-annotated with {@link Component @Component} will not be + * picked up by this annotation. * * @author Madhura Bhave * @since 2.2.0 @@ -37,7 +41,8 @@ import org.springframework.core.annotation.AliasFor; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented -@Import({ ConfigurationPropertiesScanRegistrar.class, ConfigurationPropertiesBindingPostProcessorRegistrar.class }) +@Import({ ConfigurationPropertiesScanRegistrar.class }) +@EnableConfigurationProperties public @interface ConfigurationPropertiesScan { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java index cf3674ec2d..9ba5338b22 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java @@ -19,12 +19,10 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.context.TypeExcludeFilter; -import org.springframework.context.EnvironmentAware; -import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.AnnotationAttributes; @@ -43,18 +41,23 @@ import org.springframework.util.StringUtils; * {@link ConfigurationProperties @ConfigurationProperties} bean definitions via scanning. * * @author Madhura Bhave + * @author Phillip Webb */ -class ConfigurationPropertiesScanRegistrar - implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware { +class ConfigurationPropertiesScanRegistrar implements ImportBeanDefinitionRegistrar { - private Environment environment; + private final Environment environment; - private ResourceLoader resourceLoader; + private final ResourceLoader resourceLoader; + + ConfigurationPropertiesScanRegistrar(Environment environment, ResourceLoader resourceLoader) { + this.environment = environment; + this.resourceLoader = resourceLoader; + } @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Set packagesToScan = getPackagesToScan(importingClassMetadata); - register(registry, (ConfigurableListableBeanFactory) registry, packagesToScan); + scan(registry, packagesToScan); } private Set getPackagesToScan(AnnotationMetadata metadata) { @@ -69,43 +72,43 @@ class ConfigurationPropertiesScanRegistrar if (packagesToScan.isEmpty()) { packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName())); } + packagesToScan.removeIf((candidate) -> !StringUtils.hasText(candidate)); return packagesToScan; } - protected void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory, - Set packagesToScan) { - scan(packagesToScan, beanFactory, registry); + private void scan(BeanDefinitionRegistry registry, Set packages) { + ConfigurationPropertiesBeanRegistrar registrar = new ConfigurationPropertiesBeanRegistrar(registry); + ClassPathScanningCandidateComponentProvider scanner = getScanner(registry); + for (String basePackage : packages) { + for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) { + register(registrar, candidate.getBeanClassName()); + } + } } - protected void scan(Set packages, ConfigurableListableBeanFactory beanFactory, - BeanDefinitionRegistry registry) { + private ClassPathScanningCandidateComponentProvider getScanner(BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); scanner.setEnvironment(this.environment); scanner.setResourceLoader(this.resourceLoader); scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigurationProperties.class)); TypeExcludeFilter typeExcludeFilter = new TypeExcludeFilter(); - typeExcludeFilter.setBeanFactory(beanFactory); + typeExcludeFilter.setBeanFactory((BeanFactory) registry); scanner.addExcludeFilter(typeExcludeFilter); - for (String basePackage : packages) { - if (StringUtils.hasText(basePackage)) { - scan(beanFactory, registry, scanner, basePackage); - } + return scanner; + } + + private void register(ConfigurationPropertiesBeanRegistrar registrar, String className) throws LinkageError { + try { + register(registrar, ClassUtils.forName(className, null)); + } + catch (ClassNotFoundException ex) { + // Ignore } } - private void scan(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry, - ClassPathScanningCandidateComponentProvider scanner, String basePackage) throws LinkageError { - for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) { - String beanClassName = candidate.getBeanClassName(); - try { - Class type = ClassUtils.forName(beanClassName, null); - if (!isComponent(type)) { - ConfigurationPropertiesBeanDefinitionRegistrar.register(registry, beanFactory, type); - } - } - catch (ClassNotFoundException ex) { - // Ignore - } + private void register(ConfigurationPropertiesBeanRegistrar registrar, Class type) { + if (!isComponent(type)) { + registrar.register(type); } } @@ -113,14 +116,4 @@ class ConfigurationPropertiesScanRegistrar return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(Component.class); } - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java new file mode 100644 index 0000000000..e0374fb670 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesValueObjectBeanDefinition.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.Annotation; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.annotation.Validated; + +/** + * {@link BeanDefinition} that is used for registering + * {@link ConfigurationProperties @ConfigurationProperties} value object beans that are + * bound at creation time. + * + * @author Stephane Nicoll + * @author Madhura Bhave + */ +final class ConfigurationPropertiesValueObjectBeanDefinition extends GenericBeanDefinition { + + private final BeanFactory beanFactory; + + private final String beanName; + + ConfigurationPropertiesValueObjectBeanDefinition(BeanFactory beanFactory, String beanName, Class beanClass) { + this.beanFactory = beanFactory; + this.beanName = beanName; + setBeanClass(beanClass); + setInstanceSupplier(this::createBean); + } + + private Object createBean() { + ConfigurationPropertiesBinder binder = ConfigurationPropertiesBinder.get(this.beanFactory); + ResolvableType type = ResolvableType.forClass(getBeanClass()); + ConfigurationProperties annotation = AnnotationUtils.findAnnotation(getBeanClass(), + ConfigurationProperties.class); + Validated validated = AnnotationUtils.findAnnotation(getBeanClass(), Validated.class); + Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated } + : new Annotation[] { annotation }; + Bindable bindTarget = Bindable.of(type).withAnnotations(annotations); + try { + return binder.bindOrCreate(bindTarget); + } + catch (Exception ex) { + throw new ConfigurationPropertiesBindException(this.beanName, getBeanClass(), annotation, ex); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java index 74f8c6de8b..f16136cb2b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationProperties.java @@ -37,9 +37,15 @@ import org.springframework.context.annotation.Import; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented -@Import({ ConfigurationPropertiesBeanRegistrar.class, ConfigurationPropertiesBindingPostProcessorRegistrar.class }) +@Import(EnableConfigurationPropertiesRegistrar.class) public @interface EnableConfigurationProperties { + /** + * The bean name of the configuration properties validator. + * @since 2.2.0 + */ + String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; + /** * Convenient way to quickly register * {@link ConfigurationProperties @ConfigurationProperties} annotated beans with diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java new file mode 100644 index 0000000000..ff0c1fe78b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrar.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.AnnotationMetadata; + +/** + * {@link ImportBeanDefinitionRegistrar} for + * {@link EnableConfigurationProperties @EnableConfigurationProperties}. + * + * @author Phillip Webb + */ +class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + registerInfrastructureBeans(registry); + ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry); + getTypes(metadata).forEach(beanRegistrar::register); + } + + private Set> getTypes(AnnotationMetadata metadata) { + return metadata.getAnnotations().stream(EnableConfigurationProperties.class) + .flatMap((annotation) -> Arrays.stream(annotation.getClassArray(MergedAnnotation.VALUE))) + .filter((type) -> void.class != type).collect(Collectors.toSet()); + } + + @SuppressWarnings("deprecation") + static void registerInfrastructureBeans(BeanDefinitionRegistry registry) { + ConfigurationPropertiesBindingPostProcessor.register(registry); + ConfigurationBeanFactoryMetadata.register(registry); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java index 793ad51eb2..deac5534b1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java @@ -27,6 +27,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; /** * Utility to deduce the {@link PropertySources} to use for configuration binding. @@ -49,19 +50,9 @@ class PropertySourcesDeducer { return configurer.getAppliedPropertySources(); } MutablePropertySources sources = extractEnvironmentPropertySources(); - if (sources != null) { - return sources; - } - throw new IllegalStateException( + Assert.state(sources != null, "Unable to obtain PropertySources from PropertySourcesPlaceholderConfigurer or Environment"); - } - - private MutablePropertySources extractEnvironmentPropertySources() { - Environment environment = this.applicationContext.getEnvironment(); - if (environment instanceof ConfigurableEnvironment) { - return ((ConfigurableEnvironment) environment).getPropertySources(); - } - return null; + return sources; } private PropertySourcesPlaceholderConfigurer getSinglePropertySourcesPlaceholderConfigurer() { @@ -78,4 +69,12 @@ class PropertySourcesDeducer { return null; } + private MutablePropertySources extractEnvironmentPropertySources() { + Environment environment = this.applicationContext.getEnvironment(); + if (environment instanceof ConfigurableEnvironment) { + return ((ConfigurableEnvironment) environment).getPropertySources(); + } + return null; + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java index 26dd7d0365..53e1d6e947 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java @@ -13,130 +13,98 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.boot.context.properties; -import java.io.IOException; +package org.springframework.boot.context.properties; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ConfigurationPropertiesBeanRegistrar}. * * @author Madhura Bhave * @author Stephane Nicoll + * @author Phillip Webb */ class ConfigurationPropertiesBeanRegistrarTests { - private final ConfigurationPropertiesBeanRegistrar registrar = new ConfigurationPropertiesBeanRegistrar(); + private BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); - private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + private ConfigurationPropertiesBeanRegistrar registrar = new ConfigurationPropertiesBeanRegistrar(this.registry); @Test - void typeWithDefaultConstructorShouldRegisterGenericBeanDefinition() throws Exception { - this.registrar.registerBeanDefinitions(getAnnotationMetadata(TestConfiguration.class), this.beanFactory); - BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( - "foo-org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrarTests$FooProperties"); - assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + void registerWhenNotAlreadyRegisteredAddBeanDefinition() { + String beanName = "beancp-" + BeanConfigurationProperties.class.getName(); + this.registrar.register(BeanConfigurationProperties.class); + BeanDefinition definition = this.registry.getBeanDefinition(beanName); + assertThat(definition).isNotNull(); + assertThat(definition.getBeanClassName()).isEqualTo(BeanConfigurationProperties.class.getName()); } @Test - void typeWithOneConstructorWithParametersShouldRegisterConfigurationPropertiesBeanDefinition() throws Exception { - this.registrar.registerBeanDefinitions(getAnnotationMetadata(TestConfiguration.class), this.beanFactory); - BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( - "bar-org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrarTests$BarProperties"); - assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + void registerWhenAlreadyContainsNameDoesNotReplace() { + String beanName = "beancp-" + BeanConfigurationProperties.class.getName(); + this.registry.registerBeanDefinition(beanName, new GenericBeanDefinition()); + this.registrar.register(BeanConfigurationProperties.class); + BeanDefinition definition = this.registry.getBeanDefinition(beanName); + assertThat(definition).isNotNull(); + assertThat(definition.getBeanClassName()).isNull(); } @Test - void typeWithMultipleConstructorsShouldRegisterGenericBeanDefinition() throws Exception { - this.registrar.registerBeanDefinitions(getAnnotationMetadata(TestConfiguration.class), this.beanFactory); - BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition( - "bing-org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrarTests$BingProperties"); - assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + void registerWhenNoAnnotationThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> this.registrar.register(NoAnnotationConfigurationProperties.class)) + .withMessageContaining("No ConfigurationProperties annotation found"); } @Test - void typeWithNoAnnotationShouldFail() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.registrar - .registerBeanDefinitions(getAnnotationMetadata(InvalidConfiguration.class), this.beanFactory)) - .withMessageContaining("No ConfigurationProperties annotation found") - .withMessageContaining(ConfigurationPropertiesBeanRegistrar.class.getName()); + void registerWhenValueObjectRegistersValueObjectBeanDefinition() { + String beanName = "valuecp-" + ValueObjectConfigurationProperties.class.getName(); + this.registrar.register(ValueObjectConfigurationProperties.class); + BeanDefinition definition = this.registry.getBeanDefinition(beanName); + assertThat(definition).isInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); } @Test - void registrationWithDuplicatedTypeShouldRegisterSingleBeanDefinition() throws IOException { - DefaultListableBeanFactory factory = spy(this.beanFactory); - this.registrar.registerBeanDefinitions(getAnnotationMetadata(DuplicateConfiguration.class), factory); - verify(factory, times(1)).registerBeanDefinition(anyString(), any()); + void registerWhenNotValueObjectRegistersGenericBeanDefinition() { + String beanName = MultiConstructorBeanConfigurationProperties.class.getName(); + this.registrar.register(MultiConstructorBeanConfigurationProperties.class); + BeanDefinition definition = this.registry.getBeanDefinition(beanName); + assertThat(definition).isInstanceOf(GenericBeanDefinition.class); } - @Test - void registrationWithNoTypeShouldNotRegisterAnything() throws IOException { - DefaultListableBeanFactory factory = spy(this.beanFactory); - this.registrar.registerBeanDefinitions(getAnnotationMetadata(EmptyConfiguration.class), factory); - verifyZeroInteractions(factory); - } - - private AnnotationMetadata getAnnotationMetadata(Class source) throws IOException { - return new SimpleMetadataReaderFactory().getMetadataReader(source.getName()).getAnnotationMetadata(); - } - - @EnableConfigurationProperties({ FooProperties.class, BarProperties.class, BingProperties.class }) - static class TestConfiguration { + @ConfigurationProperties(prefix = "beancp") + static class BeanConfigurationProperties { } - @EnableConfigurationProperties(ConfigurationPropertiesBeanRegistrarTests.class) - static class InvalidConfiguration { + static class NoAnnotationConfigurationProperties { } - @EnableConfigurationProperties({ FooProperties.class, FooProperties.class }) - static class DuplicateConfiguration { + @ConfigurationProperties(prefix = "valuecp") + static class ValueObjectConfigurationProperties { - } - - @EnableConfigurationProperties - static class EmptyConfiguration { - - } - - @ConfigurationProperties(prefix = "foo") - static class FooProperties { - - } - - @ConfigurationProperties(prefix = "bar") - static class BarProperties { - - BarProperties(String foo) { + ValueObjectConfigurationProperties(String name) { } } - @ConfigurationProperties(prefix = "bing") - static class BingProperties { + @ConfigurationProperties + static class MultiConstructorBeanConfigurationProperties { - BingProperties() { + MultiConstructorBeanConfigurationProperties() { } - BingProperties(String foo) { + MultiConstructorBeanConfigurationProperties(String name) { } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java new file mode 100644 index 0000000000..dfea006eac --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java @@ -0,0 +1,272 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesBean}. + * + * @author Phillip Webb + */ +class ConfigurationPropertiesBeanTests { + + @Test + void getAllReturnsAll() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + NonAnnotatedComponent.class, AnnotatedComponent.class, AnnotatedBeanConfiguration.class)) { + Map all = ConfigurationPropertiesBean.getAll(context); + assertThat(all).containsOnlyKeys("annotatedComponent", "annotatedBean"); + ConfigurationPropertiesBean component = all.get("annotatedComponent"); + assertThat(component.getName()).isEqualTo("annotatedComponent"); + assertThat(component.getInstance()).isInstanceOf(AnnotatedComponent.class); + assertThat(component.getAnnotation()).isNotNull(); + ConfigurationPropertiesBean bean = all.get("annotatedBean"); + assertThat(bean.getName()).isEqualTo("annotatedBean"); + assertThat(bean.getInstance()).isInstanceOf(AnnotatedBean.class); + assertThat(bean.getAnnotation()).isNotNull(); + } + } + + @Test + void getWhenNotAnnotatedReturnsNull() throws Throwable { + get(NonAnnotatedComponent.class, "nonAnnotatedComponent", + (propertiesBean) -> assertThat(propertiesBean).isNull()); + } + + @Test + void getWhenBeanIsAnnotatedReturnsBean() throws Throwable { + get(AnnotatedComponent.class, "annotatedComponent", (propertiesBean) -> { + assertThat(propertiesBean).isNotNull(); + assertThat(propertiesBean.getName()).isEqualTo("annotatedComponent"); + assertThat(propertiesBean.getInstance()).isInstanceOf(AnnotatedComponent.class); + assertThat(propertiesBean.getAnnotation().prefix()).isEqualTo("prefix"); + }); + } + + @Test + void getWhenFactoryMethodIsAnnotatedReturnsBean() throws Throwable { + get(NonAnnotatedBeanConfiguration.class, "nonAnnotatedBean", (propertiesBean) -> { + assertThat(propertiesBean).isNotNull(); + assertThat(propertiesBean.getName()).isEqualTo("nonAnnotatedBean"); + assertThat(propertiesBean.getInstance()).isInstanceOf(NonAnnotatedBean.class); + assertThat(propertiesBean.getAnnotation().prefix()).isEqualTo("prefix"); + }); + } + + @Test + void getWhenHasFactoryMethodBindsUsingMethodReturnType() throws Throwable { + get(NonAnnotatedGenericBeanConfiguration.class, "nonAnnotatedGenericBean", (propertiesBean) -> { + ResolvableType type = propertiesBean.asBindTarget().getType(); + assertThat(type.resolve()).isEqualTo(NonAnnotatedGenericBean.class); + assertThat(type.resolveGeneric(0)).isEqualTo(String.class); + }); + } + + @Test + void getWhenHasFactoryMethodWithoutAnnotationBindsUsingMethodType() throws Throwable { + get(AnnotatedGenericBeanConfiguration.class, "annotatedGenericBean", (propertiesBean) -> { + ResolvableType type = propertiesBean.asBindTarget().getType(); + assertThat(type.resolve()).isEqualTo(AnnotatedGenericBean.class); + assertThat(type.resolveGeneric(0)).isEqualTo(String.class); + }); + } + + @Test + void getWhenHasNoFactoryMethodBindsUsingObjectType() throws Throwable { + get(AnnotatedGenericComponent.class, "annotatedGenericComponent", (propertiesBean) -> { + ResolvableType type = propertiesBean.asBindTarget().getType(); + assertThat(type.resolve()).isEqualTo(AnnotatedGenericComponent.class); + assertThat(type.getGeneric(0).resolve()).isNull(); + }); + } + + @Test + void getWhenHasFactoryMethodAndBeanAnnotationFavorsFactoryMethod() throws Throwable { + get(AnnotatedBeanConfiguration.class, "annotatedBean", + (propertiesBean) -> assertThat(propertiesBean.getAnnotation().prefix()).isEqualTo("factory")); + } + + @Test + void getWhenHasValidatedBeanBindsWithBeanAnnotation() throws Throwable { + get(ValidatedBeanConfiguration.class, "validatedBean", (propertiesBean) -> { + Validated validated = propertiesBean.asBindTarget().getAnnotation(Validated.class); + assertThat(validated.value()).containsExactly(BeanGroup.class); + }); + } + + @Test + void getWhenHasValidatedFactoryMethodBindsWithFactoryMethodAnnotation() throws Throwable { + get(ValidatedMethodConfiguration.class, "annotatedBean", (propertiesBean) -> { + Validated validated = propertiesBean.asBindTarget().getAnnotation(Validated.class); + assertThat(validated.value()).containsExactly(FactoryMethodGroup.class); + }); + } + + @Test + void getWhenHasValidatedBeanAndFactoryMethodBindsWithFactoryMethodAnnotation() throws Throwable { + get(ValidatedMethodAndBeanConfiguration.class, "validatedBean", (propertiesBean) -> { + Validated validated = propertiesBean.asBindTarget().getAnnotation(Validated.class); + assertThat(validated.value()).containsExactly(FactoryMethodGroup.class); + }); + } + + private void get(Class configuration, String beanName, ThrowingConsumer consumer) + throws Throwable { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configuration)) { + Object bean = context.getBean(beanName); + consumer.accept(ConfigurationPropertiesBean.get(context, bean, beanName)); + } + } + + @Component("nonAnnotatedComponent") + static class NonAnnotatedComponent { + + } + + @Component("annotatedComponent") + @ConfigurationProperties(prefix = "prefix") + static class AnnotatedComponent { + + } + + @ConfigurationProperties(prefix = "prefix") + static class AnnotatedBean { + + } + + static class NonAnnotatedBean { + + } + + static class NonAnnotatedGenericBean { + + } + + @ConfigurationProperties + static class AnnotatedGenericBean { + + } + + @Component("annotatedGenericComponent") + @ConfigurationProperties + static class AnnotatedGenericComponent { + + } + + @Validated(BeanGroup.class) + @ConfigurationProperties + static class ValidatedBean { + + } + + @Configuration(proxyBeanMethods = false) + static class NonAnnotatedBeanConfiguration { + + @Bean + @ConfigurationProperties(prefix = "prefix") + NonAnnotatedBean nonAnnotatedBean() { + return new NonAnnotatedBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class NonAnnotatedGenericBeanConfiguration { + + @Bean + @ConfigurationProperties + NonAnnotatedGenericBean nonAnnotatedGenericBean() { + return new NonAnnotatedGenericBean<>(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AnnotatedGenericBeanConfiguration { + + @Bean + AnnotatedGenericBean annotatedGenericBean() { + return new AnnotatedGenericBean<>(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AnnotatedBeanConfiguration { + + @Bean + @ConfigurationProperties(prefix = "factory") + AnnotatedBean annotatedBean() { + return new AnnotatedBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ValidatedBeanConfiguration { + + @Bean + ValidatedBean validatedBean() { + return new ValidatedBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ValidatedMethodConfiguration { + + @Bean + @Validated(FactoryMethodGroup.class) + AnnotatedBean annotatedBean() { + return new AnnotatedBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ValidatedMethodAndBeanConfiguration { + + @Bean + @Validated(FactoryMethodGroup.class) + ValidatedBean validatedBean() { + return new ValidatedBean(); + } + + } + + static class BeanGroup { + + } + + static class FactoryMethodGroup { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindExceptionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindExceptionTests.java new file mode 100644 index 0000000000..7a4be60c6d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindExceptionTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesBindException}. + * + * @author Phillip Webb + */ +class ConfigurationPropertiesBindExceptionTests { + + @Test + void createFromBeanHasDetails() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Example.class); + ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.get(applicationContext, + applicationContext.getBean(Example.class), "example"); + ConfigurationPropertiesBindException exception = new ConfigurationPropertiesBindException(bean, + new IllegalStateException()); + assertThat(exception.getMessage()).isEqualTo("Error creating bean with name 'example': " + + "Could not bind properties to 'ConfigurationPropertiesBindExceptionTests.Example' : " + + "prefix=, ignoreInvalidFields=false, ignoreUnknownFields=true; " + + "nested exception is java.lang.IllegalStateException"); + assertThat(exception.getBeanType()).isEqualTo(Example.class); + assertThat(exception.getBeanName()).isEqualTo("example"); + assertThat(exception.getAnnotation()).isInstanceOf(ConfigurationProperties.class); + assertThat(exception.getCause()).isInstanceOf(IllegalStateException.class); + } + + @Test + void createFromItemsHasDetails() { + Example example = new Example(); + ConfigurationProperties annotation = example.getClass().getDeclaredAnnotation(ConfigurationProperties.class); + ConfigurationPropertiesBindException exception = new ConfigurationPropertiesBindException("example", + Example.class, annotation, new IllegalStateException()); + assertThat(exception.getMessage()).isEqualTo("Error creating bean with name 'example': " + + "Could not bind properties to 'ConfigurationPropertiesBindExceptionTests.Example' : " + + "prefix=, ignoreInvalidFields=false, ignoreUnknownFields=true; " + + "nested exception is java.lang.IllegalStateException"); + assertThat(exception.getBeanType()).isEqualTo(Example.class); + assertThat(exception.getBeanName()).isEqualTo("example"); + assertThat(exception.getAnnotation()).isInstanceOf(ConfigurationProperties.class); + assertThat(exception.getCause()).isInstanceOf(IllegalStateException.class); + } + + @Component("example") + @ConfigurationProperties + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java index 926377af21..dc88267296 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.context.properties; import java.io.IOException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; @@ -39,14 +38,10 @@ import static org.assertj.core.api.Assertions.assertThat; */ class ConfigurationPropertiesScanRegistrarTests { - private final ConfigurationPropertiesScanRegistrar registrar = new ConfigurationPropertiesScanRegistrar(); - private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - @BeforeEach - void setup() { - this.registrar.setEnvironment(new MockEnvironment()); - } + private final ConfigurationPropertiesScanRegistrar registrar = new ConfigurationPropertiesScanRegistrar( + new MockEnvironment(), null); @Test void registerBeanDefintionsShouldScanForConfigurationProperties() throws IOException { @@ -60,7 +55,7 @@ class ConfigurationPropertiesScanRegistrarTests { "bar-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$BarProperties"); assertThat(bingDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); - assertThat(barDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + assertThat(barDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); } @Test @@ -92,7 +87,7 @@ class ConfigurationPropertiesScanRegistrarTests { "b.second-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BSecondProperties"); assertThat(aDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); // Constructor injection - assertThat(bFirstDefinition).isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + assertThat(bFirstDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); // Post-processing injection assertThat(bSecondDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index b9c4436c34..5bfcca6909 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -82,7 +82,7 @@ import org.springframework.validation.annotation.Validated; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -216,7 +216,7 @@ class ConfigurationPropertiesTests { @Test void loadWhenBindingWithoutAndAnnotationShouldFail() { - assertThatIllegalArgumentException().isThrownBy(() -> load(WithoutAndAnnotationConfiguration.class, "name:foo")) + assertThatIllegalStateException().isThrownBy(() -> load(WithoutAndAnnotationConfiguration.class, "name:foo")) .withMessageContaining("No ConfigurationProperties annotation found"); } @@ -776,6 +776,7 @@ class ConfigurationPropertiesTests { } @Test + @SuppressWarnings("deprecation") void loadWhenBindingOnBeanWithoutBeanDefinitionShouldBind() { load(BasicConfiguration.class, "name=test"); BasicProperties bean = this.context.getBean(BasicProperties.class); @@ -1119,7 +1120,7 @@ class ConfigurationPropertiesTests { @EnableConfigurationProperties(WithCustomValidatorProperties.class) static class WithCustomValidatorConfiguration { - @Bean(name = ConfigurationPropertiesBindingPostProcessorRegistrar.VALIDATOR_BEAN_NAME) + @Bean(name = EnableConfigurationProperties.VALIDATOR_BEAN_NAME) CustomPropertiesValidator validator() { return new CustomPropertiesValidator(); } @@ -1130,7 +1131,7 @@ class ConfigurationPropertiesTests { @EnableConfigurationProperties(WithSetterThatThrowsValidationExceptionProperties.class) static class WithUnsupportedCustomValidatorConfiguration { - @Bean(name = ConfigurationPropertiesBindingPostProcessorRegistrar.VALIDATOR_BEAN_NAME) + @Bean(name = EnableConfigurationProperties.VALIDATOR_BEAN_NAME) CustomPropertiesValidator validator() { return new CustomPropertiesValidator(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java new file mode 100644 index 0000000000..091ee90015 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.io.InputStream; +import java.io.OutputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConversionServiceDeducer}. + * + * @author Phillip Webb + */ +class ConversionServiceDeducerTests { + + @Test + void getConversionServiceWhenHasConversionServiceBeanReturnsBean() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext( + CustomConverterServiceConfiguration.class); + ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); + assertThat(deducer.getConversionService()).isInstanceOf(TestApplicationConversionService.class); + } + + @Test + void getConversionServiceWhenHasNoConversionServiceBeanAndNoQualifiedBeansReturnsSharedInstance() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmptyConfiguration.class); + ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); + assertThat(deducer.getConversionService()).isSameAs(ApplicationConversionService.getSharedInstance()); + } + + @Test + void getConversionServiceWhenHasQualifiedConverterBeansReturnsNewInstance() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext( + CustomConverterConfiguration.class); + ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); + ConversionService conversionService = deducer.getConversionService(); + assertThat(conversionService).isNotSameAs(ApplicationConversionService.getSharedInstance()); + assertThat(conversionService.canConvert(InputStream.class, OutputStream.class)).isTrue(); + } + + @Configuration(proxyBeanMethods = false) + static class CustomConverterServiceConfiguration { + + @Bean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME) + TestApplicationConversionService conversionService() { + return new TestApplicationConversionService(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class EmptyConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class CustomConverterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + TestConveter testConveter() { + return new TestConveter(); + } + + } + + private static class TestApplicationConversionService extends ApplicationConversionService { + + } + + private static class TestConveter implements Converter { + + @Override + public OutputStream convert(InputStream source) { + throw new UnsupportedOperationException(); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java new file mode 100644 index 0000000000..931b1c0e5d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link EnableConfigurationPropertiesRegistrar}. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +class EnableConfigurationPropertiesRegistrarTests { + + private DefaultListableBeanFactory beanFactory; + + private EnableConfigurationPropertiesRegistrar registrar; + + @BeforeEach + void setup() { + this.beanFactory = spy(new DefaultListableBeanFactory()); + this.registrar = new EnableConfigurationPropertiesRegistrar(); + } + + @Test + void typeWithDefaultConstructorShouldRegisterGenericBeanDefinition() throws Exception { + register(TestConfiguration.class); + BeanDefinition beanDefinition = this.beanFactory + .getBeanDefinition("foo-" + getClass().getName() + "$FooProperties"); + assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + @Test + void typeWithOneConstructorWithParametersShouldRegisterConfigurationPropertiesBeanDefinition() throws Exception { + register(TestConfiguration.class); + BeanDefinition beanDefinition = this.beanFactory + .getBeanDefinition("bar-" + getClass().getName() + "$BarProperties"); + assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class); + } + + @Test + void typeWithMultipleConstructorsShouldRegisterGenericBeanDefinition() throws Exception { + register(TestConfiguration.class); + BeanDefinition beanDefinition = this.beanFactory + .getBeanDefinition("bing-" + getClass().getName() + "$BingProperties"); + assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + @Test + void typeWithNoAnnotationShouldFail() { + assertThatIllegalStateException().isThrownBy(() -> register(InvalidConfiguration.class)) + .withMessageContaining("No ConfigurationProperties annotation found") + .withMessageContaining(EnableConfigurationPropertiesRegistrar.class.getName()); + } + + @Test + void registrationWithDuplicatedTypeShouldRegisterSingleBeanDefinition() throws IOException { + register(DuplicateConfiguration.class); + String name = "foo-" + getClass().getName() + "$FooProperties"; + verify(this.beanFactory, times(1)).registerBeanDefinition(eq(name), any()); + } + + @Test + void registrationWithNoTypeShouldNotRegisterAnything() throws IOException { + register(EmptyConfiguration.class); + String[] names = this.beanFactory.getBeanNamesForType(Object.class); + for (String name : names) { + assertThat(name).doesNotContain("-"); + } + } + + private void register(Class configuration) { + AnnotationMetadata metadata = AnnotationMetadata.introspect(configuration); + this.registrar.registerBeanDefinitions(metadata, this.beanFactory); + } + + @EnableConfigurationProperties({ FooProperties.class, BarProperties.class, BingProperties.class }) + static class TestConfiguration { + + } + + @EnableConfigurationProperties(EnableConfigurationPropertiesRegistrarTests.class) + static class InvalidConfiguration { + + } + + @EnableConfigurationProperties({ FooProperties.class, FooProperties.class }) + static class DuplicateConfiguration { + + } + + @EnableConfigurationProperties + static class EmptyConfiguration { + + } + + @ConfigurationProperties(prefix = "foo") + static class FooProperties { + + } + + @ConfigurationProperties(prefix = "bar") + static class BarProperties { + + BarProperties(String foo) { + } + + } + + @ConfigurationProperties(prefix = "bing") + static class BingProperties { + + BingProperties() { + } + + BingProperties(String foo) { + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertySourcesDeducerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertySourcesDeducerTests.java new file mode 100644 index 0000000000..bae6e8963d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertySourcesDeducerTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySources; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PropertySourcesDeducer}. + * + * @author Phillip Webb + */ +class PropertySourcesDeducerTests { + + @Test + void getPropertySourcesWhenHasSinglePropertySourcesPlaceholderConfigurerReturnsBean() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext( + PropertySourcesPlaceholderConfigurerConfiguration.class); + PropertySourcesDeducer deducer = new PropertySourcesDeducer(applicationContext); + PropertySources propertySources = deducer.getPropertySources(); + assertThat(propertySources.get("test")).isInstanceOf(TestPropertySource.class); + } + + @Test + void getPropertySourcesWhenHasNoPropertySourcesPlaceholderConfigurerReturnsEnvironmentSources() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmptyConfiguration.class); + ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment(); + environment.getPropertySources().addFirst(new TestPropertySource()); + PropertySourcesDeducer deducer = new PropertySourcesDeducer(applicationContext); + PropertySources propertySources = deducer.getPropertySources(); + assertThat(propertySources.get("test")).isInstanceOf(TestPropertySource.class); + } + + @Test + void getPropertySourcesWhenHasMultiplePropertySourcesPlaceholderConfigurerReturnsEnvironmentSources() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext( + MultiplePropertySourcesPlaceholderConfigurerConfiguration.class); + ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment(); + environment.getPropertySources().addFirst(new TestPropertySource()); + PropertySourcesDeducer deducer = new PropertySourcesDeducer(applicationContext); + PropertySources propertySources = deducer.getPropertySources(); + assertThat(propertySources.get("test")).isInstanceOf(TestPropertySource.class); + } + + @Test + void getPropertySourcesWhenUnavailableThrowsException() { + ApplicationContext applicationContext = mock(ApplicationContext.class); + Environment environment = mock(Environment.class); + given(applicationContext.getEnvironment()).willReturn(environment); + PropertySourcesDeducer deducer = new PropertySourcesDeducer(applicationContext); + assertThatIllegalStateException().isThrownBy(() -> deducer.getPropertySources()).withMessage( + "Unable to obtain PropertySources from PropertySourcesPlaceholderConfigurer or Environment"); + } + + @Configuration(proxyBeanMethods = false) + static class PropertySourcesPlaceholderConfigurerConfiguration { + + @Bean + PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new TestPropertySource()); + configurer.setPropertySources(propertySources); + return configurer; + } + + } + + @Configuration(proxyBeanMethods = false) + static class EmptyConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class MultiplePropertySourcesPlaceholderConfigurerConfiguration { + + @Bean + PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer1() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer2() { + return new PropertySourcesPlaceholderConfigurer(); + } + + } + + private static class TestPropertySource extends MapPropertySource { + + TestPropertySource() { + super("test", Collections.emptyMap()); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt index 4e0940c53d..acf4449ecf 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt @@ -15,14 +15,13 @@ import org.springframework.core.type.classreading.SimpleMetadataReaderFactory @Suppress("unused") class KotlinConfigurationPropertiesBeanRegistrarTests { - private val registrar = ConfigurationPropertiesBeanRegistrar() - private val beanFactory = DefaultListableBeanFactory() + private val registrar = ConfigurationPropertiesBeanRegistrar(beanFactory) + @Test fun `type with default constructor should register generic bean definition`() { - this.registrar.registerBeanDefinitions( - getAnnotationMetadata(TestConfiguration::class.java), this.beanFactory) + this.registrar.register(FooProperties::class.java) val beanDefinition = this.beanFactory.getBeanDefinition( "foo-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$FooProperties") assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition::class.java) @@ -30,33 +29,21 @@ class KotlinConfigurationPropertiesBeanRegistrarTests { @Test fun `type with primary constructor and no autowired should register configuration properties bean definition`() { - this.registrar.registerBeanDefinitions( - getAnnotationMetadata(TestConfiguration::class.java), this.beanFactory) + this.registrar.register(BarProperties::class.java) val beanDefinition = this.beanFactory.getBeanDefinition( "bar-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BarProperties") assertThat(beanDefinition).isExactlyInstanceOf( - ConfigurationPropertiesBeanDefinition::class.java) + ConfigurationPropertiesValueObjectBeanDefinition::class.java) } @Test fun `type with no primary constructor should register generic bean definition`() { - this.registrar.registerBeanDefinitions( - getAnnotationMetadata(TestConfiguration::class.java), this.beanFactory) + this.registrar.register(BingProperties::class.java) val beanDefinition = this.beanFactory.getBeanDefinition( "bing-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BingProperties") assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition::class.java) } - private fun getAnnotationMetadata(source: Class<*>): AnnotationMetadata { - return SimpleMetadataReaderFactory().getMetadataReader(source.name) - .annotationMetadata - } - - - @EnableConfigurationProperties(FooProperties::class, BarProperties::class, - BingProperties::class) - class TestConfiguration - @ConfigurationProperties(prefix = "foo") class FooProperties