Add support for configuration properties scanning

See gh-12602
pull/16294/head
Madhura Bhave 6 years ago
parent 711169aa8a
commit 8f693a0277

@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter; import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter; 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 * Indicates a {@link Configuration configuration} class that declares one or more
* {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
* auto-configuration} and {@link ComponentScan component scanning}. This is a convenience * auto-configuration}, {@link ComponentScan component scanning}, and
* annotation that is equivalent to declaring {@code @Configuration}, * {@link ConfigurationPropertiesScan configuration properties scanning}. This is a
* {@code @EnableAutoConfiguration} and {@code @ComponentScan}. * convenience annotation that is equivalent to declaring {@code @Configuration},
* {@code @EnableAutoConfiguration}, {@code @ComponentScan}, and
* {@code @ConfigurationPropertiesScan}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll * @author Stephane Nicoll
@ -50,6 +53,7 @@ import org.springframework.core.annotation.AliasFor;
@Inherited @Inherited
@SpringBootConfiguration @SpringBootConfiguration
@EnableAutoConfiguration @EnableAutoConfiguration
@ConfigurationPropertiesScan
@ComponentScan(excludeFilters = { @ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

@ -1070,7 +1070,23 @@ missing property.
[[boot-features-external-config-enabling]] [[boot-features-external-config-enabling]]
==== Enabling `@ConfigurationProperties`-annotated types ==== Enabling `@ConfigurationProperties`-annotated types
Spring Boot provides an infrastructure to bind such types and register them as beans 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: shown in the following example:
[source,java,indent=0] [source,java,indent=0]
@ -1083,13 +1099,13 @@ shown in the following example:
[NOTE] [NOTE]
==== ====
When the `@ConfigurationProperties` bean is registered that way, the bean has a When the `@ConfigurationProperties` bean is registered using scanning or via
conventional name: `<prefix>-<fqn>`, where `<prefix>` is the environment key prefix `@EnableConfigurationProperties`, the bean has a conventional name: `<prefix>-<fqn>`,
specified in the `@ConfigurationProperties` annotation and `<fqn>` is the fully qualified where `<prefix>` is the environment key prefix specified in the `@ConfigurationProperties`
name of the bean. If the annotation does not provide any prefix, only the fully qualified annotation and `<fqn>` is the fully qualified name of the bean. If the annotation does not
name of the bean is used. 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 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 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`). 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 NOTE: Annotating a `@ConfigurationProperties` type with `@Component` will result in two
yourself. For instance, instead of annotating `MyConfiguration` with beans of the same type if the type is also scanned as part of classpath scanning. If you want
`@EnableConfigurationProperties(AcmeProperties.class)`, you could make `AcmeProperties` to register the bean yourself using `@Component`, consider disabling scanning of
a bean, as shown in the following example: `@ConfigurationProperties`.
[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
}
----

@ -336,8 +336,8 @@ best practices that help.
When a class does not include a `package` declaration, it is considered to be in the 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 "`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 should be avoided. It can cause particular problems for Spring Boot applications that use
the `@ComponentScan`, `@EntityScan`, or `@SpringBootApplication` annotations, since every the `@ComponentScan`, `@ConfigurationPropertiesScan`, `@EntityScan`, or `@SpringBootApplication`
class from every jar is read. annotations, since every class from every jar is read.
TIP: We recommend that you follow Java's recommended package naming conventions and use a TIP: We recommend that you follow Java's recommended package naming conventions and use a
reversed domain name (for example, `com.example.project`). 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. scan to apply only on your project.
TIP: If you don't want to use `@SpringBootApplication`, the `@EnableAutoConfiguration` TIP: If you don't want to use `@SpringBootApplication`, the `@EnableAutoConfiguration`
and `@ComponentScan` annotations that it imports defines that behaviour so you can also `@ComponentScan`, and `@ConfigurationPropertiesScan` annotations that it imports defines
use that instead. that behaviour so you can also use those instead.
The following listing shows a typical layout: 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>> auto-configuration mechanism>>
* `@ComponentScan`: enable `@Component` scan on the package where the application is * `@ComponentScan`: enable `@Component` scan on the package where the application is
located (see <<using-boot-structuring-your-code,the best practices>>) located (see <<using-boot-structuring-your-code,the best practices>>)
* `@ConfigurationPropertiesScan`: enable `@ConfigurationProperties` scan on the package
where the application is located (see <<using-boot-structuring-your-code,the best practices>>)
* `@Configuration`: allow to register extra beans in the context or import additional * `@Configuration`: allow to register extra beans in the context or import additional
configuration classes configuration classes
The `@SpringBootApplication` annotation is equivalent to using `@Configuration`, The `@SpringBootApplication` annotation is equivalent to using `@Configuration`,
`@EnableAutoConfiguration`, and `@ComponentScan` with their default attributes, as shown `@EnableAutoConfiguration`, @ComponentScan`, and `@ConfigurationPropertiesScan` with their default
in the following example: attributes, as shown in the following example:
[source,java,indent=0] [source,java,indent=0]
@ -571,7 +573,7 @@ in the following example:
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan @ConfigurationPropertiesScan
public class Application { public class Application {
public static void main(String[] args) { 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 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 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] [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 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 `@Component`-annotated classes and `@ConfigurationProperties`-annotated classes are not detected
are imported explicitly (see `@Import`). automatically and the user-defined beans are imported explicitly (see `@Import`).
==== ====

@ -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<Constructor<?>> constructors = determineConstructors(type);
return (constructors.size() == 1 && constructors.get(0).getParameterCount() > 0);
}
private static List<Constructor<?>> determineConstructors(Class<?> type) {
List<Constructor<?>> 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;
}
}

@ -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.
* <p>
* 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.
* <p>
* 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 {};
}

@ -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<String> packagesToScan = getPackagesToScan(importingClassMetadata);
register(registry, (ConfigurableListableBeanFactory) registry, packagesToScan);
}
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata
.getAnnotationAttributes(ConfigurationPropertiesScan.class.getName()));
String[] basePackages = attributes.getStringArray("basePackages");
Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
Set<String> 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<String> packagesToScan) {
scan(packagesToScan, beanFactory, registry);
}
protected void scan(Set<String> 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
}
}
}
}
}
}

@ -16,27 +16,17 @@
package org.springframework.boot.context.properties; package org.springframework.boot.context.properties;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; 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.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; 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.ImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.ImportSelector; 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.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/** /**
* Import selector that sets up binding of external properties to configuration classes * 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 { class EnableConfigurationPropertiesImportSelector implements ImportSelector {
private static boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent();
private static final String[] IMPORTS = { private static final String[] IMPORTS = {
ConfigurationPropertiesBeanRegistrar.class.getName(), ConfigurationPropertiesBeanRegistrar.class.getName(),
ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() }; ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
@ -74,8 +62,10 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector {
@Override @Override
public void registerBeanDefinitions(AnnotationMetadata metadata, public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) { BeanDefinitionRegistry registry) {
getTypes(metadata).forEach((type) -> register(registry, ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) registry;
(ConfigurableListableBeanFactory) registry, type)); getTypes(metadata)
.forEach((type) -> ConfigurationPropertiesBeanDefinitionRegistrar
.register(registry, beanFactory, type));
} }
private List<Class<?>> getTypes(AnnotationMetadata metadata) { private List<Class<?>> getTypes(AnnotationMetadata metadata) {
@ -92,83 +82,6 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector {
.collect(Collectors.toList()); .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<Constructor<?>> constructors = determineConstructors(type);
return (constructors.size() == 1
&& constructors.get(0).getParameterCount() > 0);
}
private List<Constructor<?>> determineConstructors(Class<?> type) {
List<Constructor<?>> 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;
}
} }
} }

@ -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();
}
}

@ -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) {
}
}
}

@ -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 {
}
}

@ -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 {
}
}
Loading…
Cancel
Save