Skip to content

Commit 8f693a0

Browse files
committed
Add support for configuration properties scanning
See gh-12602
1 parent 711169a commit 8f693a0

File tree

11 files changed

+563
-132
lines changed

11 files changed

+563
-132
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.springframework.boot.SpringBootConfiguration;
2727
import org.springframework.boot.context.TypeExcludeFilter;
28+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
2829
import org.springframework.context.annotation.Bean;
2930
import org.springframework.context.annotation.ComponentScan;
3031
import org.springframework.context.annotation.ComponentScan.Filter;
@@ -35,9 +36,11 @@
3536
/**
3637
* Indicates a {@link Configuration configuration} class that declares one or more
3738
* {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
38-
* auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
39-
* annotation that is equivalent to declaring {@code @Configuration},
40-
* {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
39+
* auto-configuration}, {@link ComponentScan component scanning}, and
40+
* {@link ConfigurationPropertiesScan configuration properties scanning}. This is a
41+
* convenience annotation that is equivalent to declaring {@code @Configuration},
42+
* {@code @EnableAutoConfiguration}, {@code @ComponentScan}, and
43+
* {@code @ConfigurationPropertiesScan}.
4144
*
4245
* @author Phillip Webb
4346
* @author Stephane Nicoll
@@ -50,6 +53,7 @@
5053
@Inherited
5154
@SpringBootConfiguration
5255
@EnableAutoConfiguration
56+
@ConfigurationPropertiesScan
5357
@ComponentScan(excludeFilters = {
5458
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
5559
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,23 @@ missing property.
10701070
[[boot-features-external-config-enabling]]
10711071
==== Enabling `@ConfigurationProperties`-annotated types
10721072
Spring Boot provides an infrastructure to bind such types and register them as beans
1073-
automatically. Any `@Configuration` class can specify the list of types to process as
1073+
automatically. If your application uses `@SpringBootApplication`, classes annotated with
1074+
`@ConfigurationProperties` will automatically be scanned and registered as beans. By default,
1075+
scanning will occur from the package of the class that declares this annotation. If you want
1076+
to define specific packages to scan, you can do so using an explicit `@ConfigurationPropertiesScan`
1077+
directive on your `@SpringBootApplication`-annotated class as shown in the following example:
1078+
1079+
[source,java,indent=0]
1080+
----
1081+
@SpringBootApplication
1082+
@ConfigurationPropertiesScan({ "com.example.app", "org.acme.another" })
1083+
public class MyApplication {
1084+
}
1085+
----
1086+
1087+
Sometimes, classes annotated with `@ConfigurationProperties` might not be suitable
1088+
for scanning, for example, if you're developing your own auto-configuration. In these
1089+
cases, you can specify the list of types to process on any `@Configuration` class as
10741090
shown in the following example:
10751091

10761092
[source,java,indent=0]
@@ -1083,13 +1099,13 @@ shown in the following example:
10831099

10841100
[NOTE]
10851101
====
1086-
When the `@ConfigurationProperties` bean is registered that way, the bean has a
1087-
conventional name: `<prefix>-<fqn>`, where `<prefix>` is the environment key prefix
1088-
specified in the `@ConfigurationProperties` annotation and `<fqn>` is the fully qualified
1089-
name of the bean. If the annotation does not provide any prefix, only the fully qualified
1090-
name of the bean is used.
1102+
When the `@ConfigurationProperties` bean is registered using scanning or via
1103+
`@EnableConfigurationProperties`, the bean has a conventional name: `<prefix>-<fqn>`,
1104+
where `<prefix>` is the environment key prefix specified in the `@ConfigurationProperties`
1105+
annotation and `<fqn>` is the fully qualified name of the bean. If the annotation does not
1106+
provide any prefix, only the fully qualified name of the bean is used.
10911107
1092-
The bean name in the examples above is `acme-com.example.AcmeProperties`.
1108+
The bean name in the example above is `acme-com.example.AcmeProperties`.
10931109
====
10941110

10951111
We recommend that `@ConfigurationProperties` only deal with the environment and, in
@@ -1100,27 +1116,10 @@ binder that only deals with the environment.
11001116
For corner cases, setter injection can be used or any of the `*Aware` interfaces provided
11011117
by the framework (such as `EnvironmentAware` if you need access to the `Environment`).
11021118

1103-
If you find using `@EnableConfigurationProperties` tedious, you can also declare a bean
1104-
yourself. For instance, instead of annotating `MyConfiguration` with
1105-
`@EnableConfigurationProperties(AcmeProperties.class)`, you could make `AcmeProperties`
1106-
a bean, as shown in the following example:
1107-
1108-
[source,java,indent=0]
1109-
----
1110-
@Component
1111-
@ConfigurationProperties(prefix="acme")
1112-
public class AcmeProperties {
1113-
1114-
private boolean enabled;
1115-
1116-
private InetAddress remoteAddress;
1117-
1118-
private final Security security = new Security();
1119-
1120-
// ... see the preceding JavaBean properties binding example
1121-
1122-
}
1123-
----
1119+
NOTE: Annotating a `@ConfigurationProperties` type with `@Component` will result in two
1120+
beans of the same type if the type is also scanned as part of classpath scanning. If you want
1121+
to register the bean yourself using `@Component`, consider disabling scanning of
1122+
`@ConfigurationProperties`.
11241123

11251124

11261125

spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ best practices that help.
336336
When a class does not include a `package` declaration, it is considered to be in the
337337
"`default package`". The use of the "`default package`" is generally discouraged and
338338
should be avoided. It can cause particular problems for Spring Boot applications that use
339-
the `@ComponentScan`, `@EntityScan`, or `@SpringBootApplication` annotations, since every
340-
class from every jar is read.
339+
the `@ComponentScan`, `@ConfigurationPropertiesScan`, `@EntityScan`, or `@SpringBootApplication`
340+
annotations, since every class from every jar is read.
341341

342342
TIP: We recommend that you follow Java's recommended package naming conventions and use a
343343
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
355355
scan to apply only on your project.
356356

357357
TIP: If you don't want to use `@SpringBootApplication`, the `@EnableAutoConfiguration`
358-
and `@ComponentScan` annotations that it imports defines that behaviour so you can also
359-
use that instead.
358+
`@ComponentScan`, and `@ConfigurationPropertiesScan` annotations that it imports defines
359+
that behaviour so you can also use those instead.
360360

361361
The following listing shows a typical layout:
362362

@@ -556,12 +556,14 @@ be able to define extra configuration on their "application class". A single
556556
auto-configuration mechanism>>
557557
* `@ComponentScan`: enable `@Component` scan on the package where the application is
558558
located (see <<using-boot-structuring-your-code,the best practices>>)
559+
* `@ConfigurationPropertiesScan`: enable `@ConfigurationProperties` scan on the package
560+
where the application is located (see <<using-boot-structuring-your-code,the best practices>>)
559561
* `@Configuration`: allow to register extra beans in the context or import additional
560562
configuration classes
561563

562564
The `@SpringBootApplication` annotation is equivalent to using `@Configuration`,
563-
`@EnableAutoConfiguration`, and `@ComponentScan` with their default attributes, as shown
564-
in the following example:
565+
`@EnableAutoConfiguration`, @ComponentScan`, and `@ConfigurationPropertiesScan` with their default
566+
attributes, as shown in the following example:
565567

566568

567569
[source,java,indent=0]
@@ -571,7 +573,7 @@ in the following example:
571573
import org.springframework.boot.SpringApplication;
572574
import org.springframework.boot.autoconfigure.SpringBootApplication;
573575
574-
@SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan
576+
@SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan @ConfigurationPropertiesScan
575577
public class Application {
576578
577579
public static void main(String[] args) {
@@ -588,7 +590,7 @@ NOTE: `@SpringBootApplication` also provides aliases to customize the attributes
588590
====
589591
None of these features are mandatory and you may choose to replace this single annotation
590592
by any of the features that it enables. For instance, you may not want to use component
591-
scan in your application:
593+
scan or configuration properties scan in your application:
592594
593595
[source,java,indent=0]
594596
----
@@ -612,8 +614,8 @@ scan in your application:
612614
----
613615
614616
In this example, `Application` is just like any other Spring Boot application except that
615-
`@Component`-annotated classes are not detected automatically and the user-defined beans
616-
are imported explicitly (see `@Import`).
617+
`@Component`-annotated classes and `@ConfigurationProperties`-annotated classes are not detected
618+
automatically and the user-defined beans are imported explicitly (see `@Import`).
617619
====
618620

619621

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.context.properties;
17+
18+
import java.lang.reflect.Constructor;
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import org.springframework.beans.BeanUtils;
24+
import org.springframework.beans.factory.BeanFactory;
25+
import org.springframework.beans.factory.config.BeanDefinition;
26+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
27+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
28+
import org.springframework.beans.factory.support.GenericBeanDefinition;
29+
import org.springframework.core.KotlinDetector;
30+
import org.springframework.core.annotation.AnnotationUtils;
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.StringUtils;
33+
34+
/**
35+
* Registers a bean definition for a type annotated with {@link ConfigurationProperties}
36+
* using the prefix of the annotation in the bean name.
37+
*
38+
* @author Madhura Bhave
39+
*/
40+
final class ConfigurationPropertiesBeanDefinitionRegistrar {
41+
42+
private ConfigurationPropertiesBeanDefinitionRegistrar() {
43+
}
44+
45+
private static final boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent();
46+
47+
public static void register(BeanDefinitionRegistry registry,
48+
ConfigurableListableBeanFactory beanFactory, Class<?> type) {
49+
String name = getName(type);
50+
if (!containsBeanDefinition(beanFactory, name)) {
51+
registerBeanDefinition(registry, beanFactory, name, type);
52+
}
53+
}
54+
55+
private static String getName(Class<?> type) {
56+
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
57+
ConfigurationProperties.class);
58+
String prefix = (annotation != null) ? annotation.prefix() : "";
59+
return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
60+
: type.getName());
61+
}
62+
63+
private static boolean containsBeanDefinition(
64+
ConfigurableListableBeanFactory beanFactory, String name) {
65+
if (beanFactory.containsBeanDefinition(name)) {
66+
return true;
67+
}
68+
BeanFactory parent = beanFactory.getParentBeanFactory();
69+
if (parent instanceof ConfigurableListableBeanFactory) {
70+
return containsBeanDefinition((ConfigurableListableBeanFactory) parent, name);
71+
}
72+
return false;
73+
}
74+
75+
private static void registerBeanDefinition(BeanDefinitionRegistry registry,
76+
ConfigurableListableBeanFactory beanFactory, String name, Class<?> type) {
77+
assertHasAnnotation(type);
78+
registry.registerBeanDefinition(name,
79+
createBeanDefinition(beanFactory, name, type));
80+
}
81+
82+
private static void assertHasAnnotation(Class<?> type) {
83+
Assert.notNull(
84+
AnnotationUtils.findAnnotation(type, ConfigurationProperties.class),
85+
() -> "No " + ConfigurationProperties.class.getSimpleName()
86+
+ " annotation found on '" + type.getName() + "'.");
87+
}
88+
89+
private static BeanDefinition createBeanDefinition(
90+
ConfigurableListableBeanFactory beanFactory, String name, Class<?> type) {
91+
if (canBindAtCreationTime(type)) {
92+
return ConfigurationPropertiesBeanDefinition.from(beanFactory, name, type);
93+
}
94+
else {
95+
GenericBeanDefinition definition = new GenericBeanDefinition();
96+
definition.setBeanClass(type);
97+
return definition;
98+
}
99+
}
100+
101+
private static boolean canBindAtCreationTime(Class<?> type) {
102+
List<Constructor<?>> constructors = determineConstructors(type);
103+
return (constructors.size() == 1 && constructors.get(0).getParameterCount() > 0);
104+
}
105+
106+
private static List<Constructor<?>> determineConstructors(Class<?> type) {
107+
List<Constructor<?>> constructors = new ArrayList<>();
108+
if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) {
109+
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
110+
if (primaryConstructor != null) {
111+
constructors.add(primaryConstructor);
112+
}
113+
}
114+
else {
115+
constructors.addAll(Arrays.asList(type.getDeclaredConstructors()));
116+
}
117+
return constructors;
118+
}
119+
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.context.properties;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.context.annotation.Import;
25+
import org.springframework.core.annotation.AliasFor;
26+
27+
/**
28+
* Configures the base packages used when scanning for {@link ConfigurationProperties}
29+
* classes. One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias
30+
* {@link #value()} may be specified to define specific packages to scan. If specific
31+
* packages are not defined scanning will occur from the package of the class with this
32+
* annotation.
33+
*
34+
* @author Madhura Bhave
35+
* @since 2.2.0
36+
* @see ConfigurationPropertiesScanRegistrar
37+
*/
38+
@Target(ElementType.TYPE)
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Documented
41+
@Import(ConfigurationPropertiesScanRegistrar.class)
42+
public @interface ConfigurationPropertiesScan {
43+
44+
/**
45+
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
46+
* declarations e.g.: {@code @ConfigurationPropertiesScan("org.my.pkg")} instead of
47+
* {@code @ConfigurationPropertiesScan(basePackages="org.my.pkg")}.
48+
* @return the base packages to scan
49+
*/
50+
@AliasFor("basePackages")
51+
String[] value() default {};
52+
53+
/**
54+
* Base packages to scan for configuration properties. {@link #value()} is an alias
55+
* for (and mutually exclusive with) this attribute.
56+
* <p>
57+
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based
58+
* package names.
59+
* @return the base packages to scan
60+
*/
61+
@AliasFor("value")
62+
String[] basePackages() default {};
63+
64+
/**
65+
* Type-safe alternative to {@link #basePackages()} for specifying the packages to
66+
* scan for configuration properties. The package of each class specified will be
67+
* scanned.
68+
* <p>
69+
* Consider creating a special no-op marker class or interface in each package that
70+
* serves no purpose other than being referenced by this attribute.
71+
* @return classes from the base packages to scan
72+
*/
73+
Class<?>[] basePackageClasses() default {};
74+
75+
}

0 commit comments

Comments
 (0)