Skip to content

Commit f260c77

Browse files
committed
Add @ImportConfigurationPropertiesBean support
Add repeatable `@ImportConfigurationPropertiesBean` annotation that can be used to import types and treat them as `@ConfigurationProperties` beans. This annotation is specifically designed to support third-party classes that can't contain any Spring annotations. Closes gh-23172
1 parent d2e67ab commit f260c77

File tree

35 files changed

+1272
-133
lines changed

35 files changed

+1272
-133
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,7 +1227,7 @@ Spring Boot provides infrastructure to bind `@ConfigurationProperties` types and
12271227
You can either enable configuration properties on a class-by-class basis or enable configuration property scanning that works in a similar manner to component scanning.
12281228

12291229
Sometimes, classes annotated with `@ConfigurationProperties` might not be suitable for scanning, for example, if you're developing your own auto-configuration or you want to enable them conditionally.
1230-
In these cases, specify the list of types to process using the `@EnableConfigurationProperties` annotation.
1230+
In these cases, specify the list of types to process using the `@EnableConfigurationProperties` or `@ImportConfigurationPropertiesBean` annotations.
12311231
This can be done on any `@Configuration` class, as shown in the following example:
12321232

12331233
[source,java,indent=0]
@@ -1253,7 +1253,7 @@ If you want to define specific packages to scan, you can do so as shown in the f
12531253

12541254
[NOTE]
12551255
====
1256-
When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties`, the bean has a conventional name: `<prefix>-<fqn>`, where `<prefix>` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `<fqn>` is the fully qualified name of the bean.
1256+
When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties` or `@ImportConfigurationPropertiesBean`, the bean has a conventional name: `<prefix>-<fqn>`, where `<prefix>` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `<fqn>` is the fully qualified name of the bean.
12571257
If the annotation does not provide any prefix, only the fully qualified name of the bean is used.
12581258
12591259
The bean name in the example above is `acme-com.example.AcmeProperties`.
@@ -1325,12 +1325,26 @@ To configure a bean from the `Environment` properties, add `@ConfigurationProper
13251325
----
13261326
@ConfigurationProperties(prefix = "another")
13271327
@Bean
1328-
public AnotherComponent anotherComponent() {
1328+
public ExampleItem exampleItem() {
13291329
...
13301330
}
13311331
----
13321332

1333-
Any JavaBean property defined with the `another` prefix is mapped onto that `AnotherComponent` bean in manner similar to the preceding `AcmeProperties` example.
1333+
Any JavaBean property defined with the `another` prefix is mapped onto that `ExampleItem` bean in manner similar to the preceding `AcmeProperties` example.
1334+
1335+
If you want to use constructor binding with a third-party class, you can't use a `@Bean` method since Spring will need to create the object instance.
1336+
For those situations, you can use an `@ImportConfigurationPropertiesBean` annotation on your `@Configuration` or `@SpringBootApplication` class.
1337+
1338+
[source,java,indent=0]
1339+
----
1340+
@SpringBootApplication
1341+
@ImportConfigurationPropertiesBean(type = ExampleItem.class, prefix = "another")
1342+
public class MyApp {
1343+
...
1344+
}
1345+
----
1346+
1347+
TIP: `@ImportConfigurationPropertiesBean` also works for JavaBean bindings as long as the type has a single no-arg constructor
13341348

13351349

13361350

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.time.Duration;
2323
import java.util.Collections;
2424
import java.util.LinkedHashMap;
25+
import java.util.LinkedHashSet;
2526
import java.util.List;
2627
import java.util.Map;
2728
import java.util.Set;
@@ -40,6 +41,7 @@
4041
import javax.lang.model.element.TypeElement;
4142
import javax.lang.model.element.VariableElement;
4243
import javax.lang.model.type.TypeKind;
44+
import javax.lang.model.type.TypeMirror;
4345
import javax.lang.model.util.ElementFilter;
4446
import javax.tools.Diagnostic.Kind;
4547

@@ -60,29 +62,28 @@
6062
@SupportedAnnotationTypes({ "*" })
6163
public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor {
6264

63-
static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot."
64-
+ "configurationprocessor.additionalMetadataLocations";
65+
static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot.configurationprocessor.additionalMetadataLocations";
6566

66-
static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot."
67-
+ "context.properties.ConfigurationProperties";
67+
static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties";
6868

69-
static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot."
70-
+ "context.properties.NestedConfigurationProperty";
69+
static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.NestedConfigurationProperty";
7170

72-
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot."
73-
+ "context.properties.DeprecatedConfigurationProperty";
71+
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty";
7472

7573
static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.context.properties.ConstructorBinding";
7674

7775
static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.context.properties.bind.DefaultValue";
7876

7977
static final String ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.Endpoint";
8078

81-
static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate."
82-
+ "endpoint.annotation.ReadOperation";
79+
static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.ReadOperation";
8380

8481
static final String NAME_ANNOTATION = "org.springframework.boot.context.properties.bind.Name";
8582

83+
static final String IMPORT_CONFIGURATION_PROPERTIES_BEAN_ANNOATION = "org.springframework.boot.context.properties.ImportConfigurationPropertiesBean";
84+
85+
static final String IMPORT_CONFIGURATION_PROPERTIES_BEANS_ANNOATION = "org.springframework.boot.context.properties.ImportConfigurationPropertiesBeans";
86+
8687
private static final Set<String> SUPPORTED_OPTIONS = Collections
8788
.unmodifiableSet(Collections.singleton(ADDITIONAL_METADATA_LOCATIONS_OPTION));
8889

@@ -124,6 +125,14 @@ protected String nameAnnotation() {
124125
return NAME_ANNOTATION;
125126
}
126127

128+
protected String importConfigurationPropertiesBeanAnnotation() {
129+
return IMPORT_CONFIGURATION_PROPERTIES_BEAN_ANNOATION;
130+
}
131+
132+
protected String importConfigurationPropertiesBeansAnnotation() {
133+
return IMPORT_CONFIGURATION_PROPERTIES_BEANS_ANNOATION;
134+
}
135+
127136
@Override
128137
public SourceVersion getSupportedSourceVersion() {
129138
return SourceVersion.latestSupported();
@@ -142,22 +151,16 @@ public synchronized void init(ProcessingEnvironment env) {
142151
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
143152
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
144153
constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotation(),
145-
readOperationAnnotation(), nameAnnotation());
154+
readOperationAnnotation(), nameAnnotation(), importConfigurationPropertiesBeanAnnotation(),
155+
importConfigurationPropertiesBeansAnnotation());
146156
}
147157

148158
@Override
149159
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
150160
this.metadataCollector.processing(roundEnv);
151-
TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement();
152-
if (annotationType != null) { // Is @ConfigurationProperties available
153-
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
154-
processElement(element);
155-
}
156-
}
157-
TypeElement endpointType = this.metadataEnv.getEndpointAnnotationElement();
158-
if (endpointType != null) { // Is @Endpoint available
159-
getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint);
160-
}
161+
processConfigurationProperties(roundEnv);
162+
processEndpoint(roundEnv);
163+
processImportConfigurationPropertiesBean(roundEnv);
161164
if (roundEnv.processingOver()) {
162165
try {
163166
writeMetaData();
@@ -169,6 +172,40 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
169172
return false;
170173
}
171174

175+
private void processConfigurationProperties(RoundEnvironment roundEnv) {
176+
TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement();
177+
if (annotationType != null) {
178+
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
179+
processElement(element);
180+
}
181+
}
182+
}
183+
184+
private void processEndpoint(RoundEnvironment roundEnv) {
185+
TypeElement endpointType = this.metadataEnv.getEndpointAnnotationElement();
186+
if (endpointType != null) {
187+
getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint);
188+
}
189+
}
190+
191+
private void processImportConfigurationPropertiesBean(RoundEnvironment roundEnv) {
192+
TypeElement importConfigurationPropertiesBeanType = this.metadataEnv
193+
.getImportConfigurationPropertiesBeanAnnotationElement();
194+
TypeElement importConfigurationPropertiesBeansType = this.metadataEnv
195+
.getImportConfigurationPropertiesBeansAnnotationElement();
196+
if (importConfigurationPropertiesBeanType == null && importConfigurationPropertiesBeansType == null) {
197+
return;
198+
}
199+
Set<Element> elements = new LinkedHashSet<>();
200+
if (importConfigurationPropertiesBeanType != null) {
201+
elements.addAll(roundEnv.getElementsAnnotatedWith(importConfigurationPropertiesBeanType));
202+
}
203+
if (importConfigurationPropertiesBeansType != null) {
204+
elements.addAll(roundEnv.getElementsAnnotatedWith(importConfigurationPropertiesBeansType));
205+
}
206+
elements.forEach(this::processImportConfigurationPropertiesBean);
207+
}
208+
172209
private Map<Element, List<Element>> getElementsAnnotatedOrMetaAnnotatedWith(RoundEnvironment roundEnv,
173210
TypeElement annotation) {
174211
Map<Element, List<Element>> result = new LinkedHashMap<>();
@@ -187,7 +224,7 @@ private void processElement(Element element) {
187224
if (annotation != null) {
188225
String prefix = getPrefix(annotation);
189226
if (element instanceof TypeElement) {
190-
processAnnotatedTypeElement(prefix, (TypeElement) element, new Stack<>());
227+
processAnnotatedTypeElement(prefix, (TypeElement) element, false, new Stack<>());
191228
}
192229
else if (element instanceof ExecutableElement) {
193230
processExecutableElement(prefix, (ExecutableElement) element, new Stack<>());
@@ -199,10 +236,11 @@ else if (element instanceof ExecutableElement) {
199236
}
200237
}
201238

202-
private void processAnnotatedTypeElement(String prefix, TypeElement element, Stack<TypeElement> seen) {
239+
private void processAnnotatedTypeElement(String prefix, TypeElement element, boolean fromImport,
240+
Stack<TypeElement> seen) {
203241
String type = this.metadataEnv.getTypeUtils().getQualifiedName(element);
204242
this.metadataCollector.add(ItemMetadata.newGroup(prefix, type, type, null));
205-
processTypeElement(prefix, element, null, seen);
243+
processTypeElement(prefix, element, fromImport, null, seen);
206244
}
207245

208246
private void processExecutableElement(String prefix, ExecutableElement element, Stack<TypeElement> seen) {
@@ -220,25 +258,26 @@ private void processExecutableElement(String prefix, ExecutableElement element,
220258
}
221259
else {
222260
this.metadataCollector.add(group);
223-
processTypeElement(prefix, (TypeElement) returns, element, seen);
261+
processTypeElement(prefix, (TypeElement) returns, false, element, seen);
224262
}
225263
}
226264
}
227265
}
228266

229-
private void processTypeElement(String prefix, TypeElement element, ExecutableElement source,
267+
private void processTypeElement(String prefix, TypeElement element, boolean fromImport, ExecutableElement source,
230268
Stack<TypeElement> seen) {
231269
if (!seen.contains(element)) {
232270
seen.push(element);
233-
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> {
234-
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
235-
if (descriptor.isNested(this.metadataEnv)) {
236-
TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils()
237-
.asElement(descriptor.getType());
238-
String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, descriptor.getName());
239-
processTypeElement(nestedPrefix, nestedTypeElement, source, seen);
240-
}
241-
});
271+
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, fromImport, source)
272+
.forEach((descriptor) -> {
273+
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
274+
if (descriptor.isNested(this.metadataEnv)) {
275+
TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils()
276+
.asElement(descriptor.getType());
277+
String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, descriptor.getName());
278+
processTypeElement(nestedPrefix, nestedTypeElement, false, source, seen);
279+
}
280+
});
242281
seen.pop();
243282
}
244283
}
@@ -275,6 +314,21 @@ private void processEndpoint(AnnotationMirror annotation, TypeElement element) {
275314
}
276315
}
277316

317+
private void processImportConfigurationPropertiesBean(Element element) {
318+
this.metadataEnv.getImportConfigurationPropertiesBeanAnnotations(element)
319+
.forEach(this::processImportConfigurationPropertiesBean);
320+
}
321+
322+
@SuppressWarnings("unchecked")
323+
private void processImportConfigurationPropertiesBean(AnnotationMirror annotation) {
324+
String prefix = getPrefix(annotation);
325+
List<TypeMirror> types = (List<TypeMirror>) this.metadataEnv.getAnnotationElementValues(annotation).get("type");
326+
for (TypeMirror type : types) {
327+
Element element = this.metadataEnv.getTypeUtils().asElement(type);
328+
processAnnotatedTypeElement(prefix, (TypeElement) element, true, new Stack<>());
329+
}
330+
}
331+
278332
private boolean hasMainReadOperation(TypeElement element) {
279333
for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) {
280334
if (this.metadataEnv.getReadOperationAnnotation(method) != null

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,15 @@ class MetadataGenerationEnvironment {
9797

9898
private final String nameAnnotation;
9999

100+
private final String importConfigurationPropertiesBeanAnnotation;
101+
102+
private final String importConfigurationPropertiesBeansAnnotation;
103+
100104
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
101105
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
102106
String constructorBindingAnnotation, String defaultValueAnnotation, String endpointAnnotation,
103-
String readOperationAnnotation, String nameAnnotation) {
107+
String readOperationAnnotation, String nameAnnotation, String importConfigurationPropertiesBeanAnnotation,
108+
String importConfigurationPropertiesBeansAnnotation) {
104109
this.typeUtils = new TypeUtils(environment);
105110
this.elements = environment.getElementUtils();
106111
this.messager = environment.getMessager();
@@ -113,6 +118,8 @@ class MetadataGenerationEnvironment {
113118
this.endpointAnnotation = endpointAnnotation;
114119
this.readOperationAnnotation = readOperationAnnotation;
115120
this.nameAnnotation = nameAnnotation;
121+
this.importConfigurationPropertiesBeanAnnotation = importConfigurationPropertiesBeanAnnotation;
122+
this.importConfigurationPropertiesBeansAnnotation = importConfigurationPropertiesBeansAnnotation;
116123
}
117124

118125
private static FieldValuesParser resolveFieldValuesParser(ProcessingEnvironment env) {
@@ -258,6 +265,14 @@ TypeElement getConfigurationPropertiesAnnotationElement() {
258265
return this.elements.getTypeElement(this.configurationPropertiesAnnotation);
259266
}
260267

268+
TypeElement getImportConfigurationPropertiesBeanAnnotationElement() {
269+
return this.elements.getTypeElement(this.importConfigurationPropertiesBeanAnnotation);
270+
}
271+
272+
TypeElement getImportConfigurationPropertiesBeansAnnotationElement() {
273+
return this.elements.getTypeElement(this.importConfigurationPropertiesBeansAnnotation);
274+
}
275+
261276
AnnotationMirror getConfigurationPropertiesAnnotation(Element element) {
262277
return getAnnotation(element, this.configurationPropertiesAnnotation);
263278
}
@@ -282,6 +297,22 @@ AnnotationMirror getNameAnnotation(Element element) {
282297
return getAnnotation(element, this.nameAnnotation);
283298
}
284299

300+
List<AnnotationMirror> getImportConfigurationPropertiesBeanAnnotations(Element element) {
301+
List<AnnotationMirror> annotations = new ArrayList<>();
302+
AnnotationMirror importBean = getAnnotation(element, this.importConfigurationPropertiesBeanAnnotation);
303+
if (importBean != null) {
304+
annotations.add(importBean);
305+
}
306+
AnnotationMirror importBeans = getAnnotation(element, this.importConfigurationPropertiesBeansAnnotation);
307+
if (importBeans != null) {
308+
AnnotationValue value = importBeans.getElementValues().values().iterator().next();
309+
for (Object contained : (List<?>) value.getValue()) {
310+
annotations.add((AnnotationMirror) contained);
311+
}
312+
}
313+
return Collections.unmodifiableList(annotations);
314+
}
315+
285316
boolean hasNullableAnnotation(Element element) {
286317
return getAnnotation(element, NULLABLE_ANNOTATION) != null;
287318
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,19 @@ class PropertyDescriptorResolver {
4949
* specified {@link TypeElement type} based on the specified {@link ExecutableElement
5050
* factory method}, if any.
5151
* @param type the target type
52+
* @param fromImport it the type was imported via a
53+
* {@code @ImportConfigurationPropertiesBean}
5254
* @param factoryMethod the method that triggered the metadata for that {@code type}
5355
* or {@code null}
5456
* @return the candidate properties for metadata generation
5557
*/
56-
Stream<PropertyDescriptor<?>> resolve(TypeElement type, ExecutableElement factoryMethod) {
58+
Stream<PropertyDescriptor<?>> resolve(TypeElement type, boolean fromImport, ExecutableElement factoryMethod) {
5759
TypeElementMembers members = new TypeElementMembers(this.environment, type);
5860
if (factoryMethod != null) {
5961
return resolveJavaBeanProperties(type, factoryMethod, members);
6062
}
61-
return resolve(ConfigurationPropertiesTypeElement.of(type, this.environment), factoryMethod, members);
63+
return resolve(ConfigurationPropertiesTypeElement.of(type, fromImport, this.environment), factoryMethod,
64+
members);
6265
}
6366

6467
private Stream<PropertyDescriptor<?>> resolve(ConfigurationPropertiesTypeElement type,
@@ -178,20 +181,29 @@ private ExecutableElement findBoundConstructor() {
178181
return boundConstructor;
179182
}
180183

181-
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
182-
boolean constructorBoundType = isConstructorBoundType(type, env);
184+
static ConfigurationPropertiesTypeElement of(TypeElement type, boolean fromImport,
185+
MetadataGenerationEnvironment env) {
183186
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
184187
List<ExecutableElement> boundConstructors = constructors.stream()
185188
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
189+
boolean constructorBoundType = isConstructorBoundType(type, fromImport, constructors, env);
186190
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
187191
}
188192

189-
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
193+
private static boolean isConstructorBoundType(TypeElement type, boolean fromImport,
194+
List<ExecutableElement> constructors, MetadataGenerationEnvironment env) {
190195
if (env.hasConstructorBindingAnnotation(type)) {
191196
return true;
192197
}
193198
if (type.getNestingKind() == NestingKind.MEMBER) {
194-
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env);
199+
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), false, constructors, env);
200+
}
201+
if (fromImport) {
202+
for (ExecutableElement constructor : constructors) {
203+
if (!constructor.getParameters().isEmpty()) {
204+
return true;
205+
}
206+
}
195207
}
196208
return false;
197209
}

0 commit comments

Comments
 (0)