From 8f693a027750956c0c1e36255e2b1919fe7e17c9 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 20 Mar 2019 16:14:30 -0700 Subject: [PATCH] Add support for configuration properties scanning See gh-12602 --- .../autoconfigure/SpringBootApplication.java | 10 +- .../main/asciidoc/spring-boot-features.adoc | 55 ++++---- .../src/main/asciidoc/using-spring-boot.adoc | 22 ++-- ...tionPropertiesBeanDefinitionRegistrar.java | 120 ++++++++++++++++++ .../ConfigurationPropertiesScan.java | 75 +++++++++++ .../ConfigurationPropertiesScanRegistrar.java | 91 +++++++++++++ ...ConfigurationPropertiesImportSelector.java | 95 +------------- ...igurationPropertiesScanRegistrarTests.java | 97 ++++++++++++++ ...figurationPropertiesScanConfiguration.java | 70 ++++++++++ .../properties/scan/a/AScanConfiguration.java | 30 +++++ .../properties/scan/b/BScanConfiguration.java | 30 +++++ 11 files changed, 563 insertions(+), 132 deletions(-) create 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/ConfigurationPropertiesScan.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/ConfigurationPropertiesScanConfiguration.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/a/AScanConfiguration.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/b/BScanConfiguration.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java index 96e80f3c7b..d226d04493 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java @@ -25,6 +25,7 @@ import java.lang.annotation.Target; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; @@ -35,9 +36,11 @@ import org.springframework.core.annotation.AliasFor; /** * Indicates a {@link Configuration configuration} class that declares one or more * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration - * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience - * annotation that is equivalent to declaring {@code @Configuration}, - * {@code @EnableAutoConfiguration} and {@code @ComponentScan}. + * auto-configuration}, {@link ComponentScan component scanning}, and + * {@link ConfigurationPropertiesScan configuration properties scanning}. This is a + * convenience annotation that is equivalent to declaring {@code @Configuration}, + * {@code @EnableAutoConfiguration}, {@code @ComponentScan}, and + * {@code @ConfigurationPropertiesScan}. * * @author Phillip Webb * @author Stephane Nicoll @@ -50,6 +53,7 @@ import org.springframework.core.annotation.AliasFor; @Inherited @SpringBootConfiguration @EnableAutoConfiguration +@ConfigurationPropertiesScan @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 4412a50d05..8b5ea8e622 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1070,7 +1070,23 @@ missing property. [[boot-features-external-config-enabling]] ==== Enabling `@ConfigurationProperties`-annotated types Spring Boot provides an infrastructure to bind such types and register them as beans -automatically. Any `@Configuration` class can specify the list of types to process as +automatically. If your application uses `@SpringBootApplication`, classes annotated with +`@ConfigurationProperties` will automatically be scanned and registered as beans. By default, +scanning will occur from the package of the class that declares this annotation. If you want +to define specific packages to scan, you can do so using an explicit `@ConfigurationPropertiesScan` +directive on your `@SpringBootApplication`-annotated class as shown in the following example: + +[source,java,indent=0] +---- + @SpringBootApplication + @ConfigurationPropertiesScan({ "com.example.app", "org.acme.another" }) + public class MyApplication { + } +---- + +Sometimes, classes annotated with `@ConfigurationProperties` might not be suitable +for scanning, for example, if you're developing your own auto-configuration. In these +cases, you can specify the list of types to process on any `@Configuration` class as shown in the following example: [source,java,indent=0] @@ -1083,13 +1099,13 @@ shown in the following example: [NOTE] ==== -When the `@ConfigurationProperties` bean is registered that way, the bean has a -conventional name: `-`, where `` is the environment key prefix -specified in the `@ConfigurationProperties` annotation and `` is the fully qualified -name of the bean. If the annotation does not provide any prefix, only the fully qualified -name of the bean is used. +When the `@ConfigurationProperties` bean is registered using scanning or via +`@EnableConfigurationProperties`, the bean has a conventional name: `-`, +where `` is the environment key prefix specified in the `@ConfigurationProperties` +annotation and `` is the fully qualified name of the bean. If the annotation does not +provide any prefix, only the fully qualified name of the bean is used. -The bean name in the examples above is `acme-com.example.AcmeProperties`. +The bean name in the example above is `acme-com.example.AcmeProperties`. ==== We recommend that `@ConfigurationProperties` only deal with the environment and, in @@ -1100,27 +1116,10 @@ binder that only deals with the environment. For corner cases, setter injection can be used or any of the `*Aware` interfaces provided by the framework (such as `EnvironmentAware` if you need access to the `Environment`). -If you find using `@EnableConfigurationProperties` tedious, you can also declare a bean -yourself. For instance, instead of annotating `MyConfiguration` with -`@EnableConfigurationProperties(AcmeProperties.class)`, you could make `AcmeProperties` -a bean, as shown in the following example: - -[source,java,indent=0] ----- - @Component - @ConfigurationProperties(prefix="acme") - public class AcmeProperties { - - private boolean enabled; - - private InetAddress remoteAddress; - - private final Security security = new Security(); - - // ... see the preceding JavaBean properties binding example - - } ----- +NOTE: Annotating a `@ConfigurationProperties` type with `@Component` will result in two +beans of the same type if the type is also scanned as part of classpath scanning. If you want +to register the bean yourself using `@Component`, consider disabling scanning of +`@ConfigurationProperties`. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc index ea5d46a9b2..d1fcaa6025 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc @@ -336,8 +336,8 @@ best practices that help. When a class does not include a `package` declaration, it is considered to be in the "`default package`". The use of the "`default package`" is generally discouraged and should be avoided. It can cause particular problems for Spring Boot applications that use -the `@ComponentScan`, `@EntityScan`, or `@SpringBootApplication` annotations, since every -class from every jar is read. +the `@ComponentScan`, `@ConfigurationPropertiesScan`, `@EntityScan`, or `@SpringBootApplication` +annotations, since every class from every jar is read. TIP: We recommend that you follow Java's recommended package naming conventions and use a reversed domain name (for example, `com.example.project`). @@ -355,8 +355,8 @@ is used to search for `@Entity` items. Using a root package also allows componen scan to apply only on your project. TIP: If you don't want to use `@SpringBootApplication`, the `@EnableAutoConfiguration` -and `@ComponentScan` annotations that it imports defines that behaviour so you can also -use that instead. +`@ComponentScan`, and `@ConfigurationPropertiesScan` annotations that it imports defines +that behaviour so you can also use those instead. The following listing shows a typical layout: @@ -556,12 +556,14 @@ be able to define extra configuration on their "application class". A single auto-configuration mechanism>> * `@ComponentScan`: enable `@Component` scan on the package where the application is located (see <>) +* `@ConfigurationPropertiesScan`: enable `@ConfigurationProperties` scan on the package +where the application is located (see <>) * `@Configuration`: allow to register extra beans in the context or import additional configuration classes The `@SpringBootApplication` annotation is equivalent to using `@Configuration`, -`@EnableAutoConfiguration`, and `@ComponentScan` with their default attributes, as shown -in the following example: +`@EnableAutoConfiguration`, @ComponentScan`, and `@ConfigurationPropertiesScan` with their default +attributes, as shown in the following example: [source,java,indent=0] @@ -571,7 +573,7 @@ in the following example: import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; - @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan + @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan @ConfigurationPropertiesScan public class Application { public static void main(String[] args) { @@ -588,7 +590,7 @@ NOTE: `@SpringBootApplication` also provides aliases to customize the attributes ==== None of these features are mandatory and you may choose to replace this single annotation by any of the features that it enables. For instance, you may not want to use component -scan in your application: +scan or configuration properties scan in your application: [source,java,indent=0] ---- @@ -612,8 +614,8 @@ scan in your application: ---- In this example, `Application` is just like any other Spring Boot application except that -`@Component`-annotated classes are not detected automatically and the user-defined beans -are imported explicitly (see `@Import`). +`@Component`-annotated classes and `@ConfigurationProperties`-annotated classes are not detected +automatically and the user-defined beans are imported explicitly (see `@Import`). ==== 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 new file mode 100644 index 0000000000..7327f8d741 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionRegistrar.java @@ -0,0 +1,120 @@ +/* + * 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.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Registers a bean definition for a type annotated with {@link ConfigurationProperties} + * using the prefix of the annotation in the bean name. + * + * @author Madhura Bhave + */ +final class ConfigurationPropertiesBeanDefinitionRegistrar { + + private ConfigurationPropertiesBeanDefinitionRegistrar() { + } + + private static final boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent(); + + public static void register(BeanDefinitionRegistry registry, + ConfigurableListableBeanFactory beanFactory, Class type) { + String name = getName(type); + if (!containsBeanDefinition(beanFactory, name)) { + registerBeanDefinition(registry, beanFactory, name, type); + } + } + + private static String getName(Class type) { + ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, + ConfigurationProperties.class); + String prefix = (annotation != null) ? annotation.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) { + assertHasAnnotation(type); + registry.registerBeanDefinition(name, + createBeanDefinition(beanFactory, name, type)); + } + + private static void assertHasAnnotation(Class type) { + Assert.notNull( + AnnotationUtils.findAnnotation(type, ConfigurationProperties.class), + () -> "No " + ConfigurationProperties.class.getSimpleName() + + " annotation found on '" + type.getName() + "'."); + } + + private static BeanDefinition createBeanDefinition( + ConfigurableListableBeanFactory beanFactory, String name, Class type) { + if (canBindAtCreationTime(type)) { + return ConfigurationPropertiesBeanDefinition.from(beanFactory, name, type); + } + else { + 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/ConfigurationPropertiesScan.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java new file mode 100644 index 0000000000..f7d158eef1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScan.java @@ -0,0 +1,75 @@ +/* + * 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.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; + +/** + * Configures the base packages used when scanning for {@link ConfigurationProperties} + * classes. One of {@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. + * + * @author Madhura Bhave + * @since 2.2.0 + * @see ConfigurationPropertiesScanRegistrar + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(ConfigurationPropertiesScanRegistrar.class) +public @interface ConfigurationPropertiesScan { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @ConfigurationPropertiesScan("org.my.pkg")} instead of + * {@code @ConfigurationPropertiesScan(basePackages="org.my.pkg")}. + * @return the base packages to scan + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for configuration properties. {@link #value()} is an alias + * for (and mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + * @return the base packages to scan + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for configuration properties. The package of each class specified will be + * scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return classes from the base packages to scan + */ + Class[] basePackageClasses() default {}; + +} 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 new file mode 100644 index 0000000000..a2b21cd8dd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrar.java @@ -0,0 +1,91 @@ +/* + * 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.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ImportBeanDefinitionRegistrar} for registering {@link ConfigurationProperties} + * bean definitions via scanning. + * + * @author Madhura Bhave + */ +class ConfigurationPropertiesScanRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + Set packagesToScan = getPackagesToScan(importingClassMetadata); + register(registry, (ConfigurableListableBeanFactory) registry, packagesToScan); + } + + private Set getPackagesToScan(AnnotationMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata + .getAnnotationAttributes(ConfigurationPropertiesScan.class.getName())); + String[] basePackages = attributes.getStringArray("basePackages"); + Class[] basePackageClasses = attributes.getClassArray("basePackageClasses"); + Set packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages)); + for (Class basePackageClass : basePackageClasses) { + packagesToScan.add(ClassUtils.getPackageName(basePackageClass)); + } + if (packagesToScan.isEmpty()) { + packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName())); + } + return packagesToScan; + } + + protected void register(BeanDefinitionRegistry registry, + ConfigurableListableBeanFactory beanFactory, Set packagesToScan) { + scan(packagesToScan, beanFactory, registry); + } + + protected void scan(Set packages, ConfigurableListableBeanFactory beanFactory, + BeanDefinitionRegistry registry) { + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider( + false); + scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigurationProperties.class)); + for (String basePackage : packages) { + if (StringUtils.hasText(basePackage)) { + for (BeanDefinition candidate : scanner + .findCandidateComponents(basePackage)) { + String beanClassName = candidate.getBeanClassName(); + try { + Class type = ClassUtils.forName(beanClassName, null); + ConfigurationPropertiesBeanDefinitionRegistrar.register(registry, + beanFactory, type); + } + catch (ClassNotFoundException ex) { + // Ignore + } + } + } + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java index 9cc5bfaa8b..e1281601a9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java @@ -16,27 +16,17 @@ package org.springframework.boot.context.properties; -import java.lang.reflect.Constructor; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -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.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.KotlinDetector; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; /** * Import selector that sets up binding of external properties to configuration classes @@ -54,8 +44,6 @@ import org.springframework.util.StringUtils; */ class EnableConfigurationPropertiesImportSelector implements ImportSelector { - private static boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent(); - private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(), ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() }; @@ -74,8 +62,10 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - getTypes(metadata).forEach((type) -> register(registry, - (ConfigurableListableBeanFactory) registry, type)); + ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) registry; + getTypes(metadata) + .forEach((type) -> ConfigurationPropertiesBeanDefinitionRegistrar + .register(registry, beanFactory, type)); } private List> getTypes(AnnotationMetadata metadata) { @@ -92,83 +82,6 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector { .collect(Collectors.toList()); } - private void register(BeanDefinitionRegistry registry, - ConfigurableListableBeanFactory beanFactory, Class type) { - String name = getName(type); - if (!containsBeanDefinition(beanFactory, name)) { - registerBeanDefinition(registry, beanFactory, name, type); - } - } - - private String getName(Class type) { - ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, - ConfigurationProperties.class); - String prefix = (annotation != null) ? annotation.prefix() : ""; - return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() - : type.getName()); - } - - private 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 void registerBeanDefinition(BeanDefinitionRegistry registry, - ConfigurableListableBeanFactory beanFactory, String name, Class type) { - assertHasAnnotation(type); - registry.registerBeanDefinition(name, - createBeanDefinition(beanFactory, name, type)); - } - - private void assertHasAnnotation(Class type) { - Assert.notNull( - AnnotationUtils.findAnnotation(type, ConfigurationProperties.class), - () -> "No " + ConfigurationProperties.class.getSimpleName() - + " annotation found on '" + type.getName() + "'."); - } - - private BeanDefinition createBeanDefinition( - ConfigurableListableBeanFactory beanFactory, String name, Class type) { - if (canBindAtCreationTime(type)) { - return ConfigurationPropertiesBeanDefinition.from(beanFactory, name, - type); - } - else { - GenericBeanDefinition definition = new GenericBeanDefinition(); - definition.setBeanClass(type); - return definition; - } - } - - private boolean canBindAtCreationTime(Class type) { - List> constructors = determineConstructors(type); - return (constructors.size() == 1 - && constructors.get(0).getParameterCount() > 0); - } - - private 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/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 new file mode 100644 index 0000000000..2e2d1f29d3 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesScanRegistrarTests.java @@ -0,0 +1,97 @@ +/* + * 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.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.boot.context.properties.scan.ConfigurationPropertiesScanConfiguration; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesScanRegistrar}. + * + * @author Madhura Bhave + */ +public class ConfigurationPropertiesScanRegistrarTests { + + private final ConfigurationPropertiesScanRegistrar registrar = new ConfigurationPropertiesScanRegistrar(); + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + public void registerBeanDefintionsShouldScanForConfigurationProperties() + throws IOException { + this.registrar.registerBeanDefinitions( + getAnnotationMetadata(ConfigurationPropertiesScanConfiguration.class), + this.beanFactory); + BeanDefinition bingDefinition = this.beanFactory.getBeanDefinition( + "bing-org.springframework.boot.context.properties.scan.ConfigurationPropertiesScanConfiguration$BingProperties"); + BeanDefinition fooDefinition = this.beanFactory.getBeanDefinition( + "foo-org.springframework.boot.context.properties.scan.ConfigurationPropertiesScanConfiguration$FooProperties"); + BeanDefinition barDefinition = this.beanFactory.getBeanDefinition( + "bar-org.springframework.boot.context.properties.scan.ConfigurationPropertiesScanConfiguration$BarProperties"); + assertThat(bingDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(barDefinition) + .isExactlyInstanceOf(ConfigurationPropertiesBeanDefinition.class); + } + + @Test + public void scanWhenBeanDefinitionExistsShouldSkip() throws IOException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(false); + this.registrar.registerBeanDefinitions( + getAnnotationMetadata( + ConfigurationPropertiesScanConfiguration.TestConfiguration.class), + beanFactory); + BeanDefinition fooDefinition = beanFactory.getBeanDefinition( + "foo-org.springframework.boot.context.properties.scan.ConfigurationPropertiesScanConfiguration$FooProperties"); + assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + @Test + public void scanWhenBasePackagesAndBasePackcageClassesProvidedShouldUseThat() + throws IOException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(false); + this.registrar.registerBeanDefinitions(getAnnotationMetadata( + ConfigurationPropertiesScanConfiguration.DifferentPackageConfiguration.class), + beanFactory); + assertThat(beanFactory.containsBeanDefinition( + "foo-org.springframework.boot.context.properties.scan.ConfigurationPropertiesScanConfiguration$FooProperties")) + .isFalse(); + BeanDefinition aDefinition = beanFactory.getBeanDefinition( + "a-org.springframework.boot.context.properties.scan.a.AScanConfiguration$AProperties"); + BeanDefinition bDefinition = beanFactory.getBeanDefinition( + "b-org.springframework.boot.context.properties.scan.b.BScanConfiguration$BProperties"); + assertThat(aDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + assertThat(bDefinition).isExactlyInstanceOf(GenericBeanDefinition.class); + } + + private AnnotationMetadata getAnnotationMetadata(Class source) throws IOException { + return new SimpleMetadataReaderFactory().getMetadataReader(source.getName()) + .getAnnotationMetadata(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/ConfigurationPropertiesScanConfiguration.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/ConfigurationPropertiesScanConfiguration.java new file mode 100644 index 0000000000..e8d8d70152 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/ConfigurationPropertiesScanConfiguration.java @@ -0,0 +1,70 @@ +/* + * 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.scan; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.scan.b.BScanConfiguration; + +/** + * Used for testing {@link ConfigurationProperties} scanning. + * + * @author Madhura Bhave + */ +@ConfigurationPropertiesScan +public class ConfigurationPropertiesScanConfiguration { + + @ConfigurationPropertiesScan + @EnableConfigurationProperties({ + ConfigurationPropertiesScanConfiguration.FooProperties.class }) + public static class TestConfiguration { + + } + + @ConfigurationPropertiesScan(basePackages = "org.springframework.boot.context.properties.scan.a", basePackageClasses = BScanConfiguration.class) + public static class DifferentPackageConfiguration { + + } + + @ConfigurationProperties(prefix = "foo") + static class FooProperties { + + } + + @ConfigurationProperties(prefix = "bar") + public static class BarProperties { + + public BarProperties(String foo) { + + } + + } + + @ConfigurationProperties(prefix = "bing") + public static class BingProperties { + + public BingProperties() { + + } + + public BingProperties(String foo) { + + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/a/AScanConfiguration.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/a/AScanConfiguration.java new file mode 100644 index 0000000000..02a662878e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/a/AScanConfiguration.java @@ -0,0 +1,30 @@ +/* + * 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.scan.a; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Madhura Bhave + */ +public class AScanConfiguration { + + @ConfigurationProperties(prefix = "a") + static class AProperties { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/b/BScanConfiguration.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/b/BScanConfiguration.java new file mode 100644 index 0000000000..81b924bf30 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/b/BScanConfiguration.java @@ -0,0 +1,30 @@ +/* + * 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.scan.b; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Madhura Bhave + */ +public class BScanConfiguration { + + @ConfigurationProperties(prefix = "b") + static class BProperties { + + } + +}