Skip to content

Commit b3f35ba

Browse files
committed
Add metadata support for types in arbitrary modules
Previously, if a ConfigurationProperties had a nested type or was extending from a type located outside the compilation unit, no metadata discovered on the source code was available (documentation and explicit default value, if any). This typically happens when such a type resides in another module. This commit introduces `@ConfigurationPropertiesSource` as a way to annotate such type and have metadata generated for them in their own module. Type-metadata is generated as one file per type and is reused transparently whenever that type is used. As for module metadata, an additional file can be crafted manually and will be merged when the metadata for the type is generated. The following is an example structure with two types where one has an additional metadata: META-iNF/ spring/ configuration-properties/ additional/ com.example.SourceOne.json com.example.SourceOne.json com.example.SourceTwo.json Those files are used only by the annotation processor and are not meant to be public API. See gh-18366
1 parent 2fa8d38 commit b3f35ba

File tree

55 files changed

+2548
-139
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2548
-139
lines changed

spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ With Gradle, declare the dependencies in the `annotationProcessor` configuration
8484
[[appendix.configuration-metadata.annotation-processor.automatic-metadata-generation]]
8585
== Automatic Metadata Generation
8686

87-
The processor picks up both classes and methods that are annotated with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation].
87+
The processor picks up both classes and methods that are annotated with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation]. It also picks classes that are annotated with javadoc:org.springframework.boot.context.properties.ConfigurationPropertiesSource[format=annotation]
8888

89-
NOTE: Custom annotations that are meta-annotated with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] are not supported.
89+
NOTE: Custom annotations that are meta-annotated with either of those annotations are not supported.
9090

9191
If the class has a single parameterized constructor, one property is created per constructor parameter, unless the constructor is annotated with javadoc:org.springframework.beans.factory.annotation.Autowired[format=annotation].
9292
If the class has a constructor explicitly annotated with javadoc:org.springframework.boot.context.properties.bind.ConstructorBinding[format=annotation], one property is created per constructor parameter for that constructor.
@@ -100,9 +100,10 @@ include-code::MyServerProperties[]
100100
This exposes three properties where `my.server.name` has no default and `my.server.ip` and `my.server.port` defaults to `"127.0.0.1"` and `9797` respectively.
101101
The Javadoc on fields is used to populate the `description` attribute.
102102
For instance, the description of `my.server.ip` is "IP address to listen to.".
103+
103104
The `description` attribute can only be populated when the type is available as source code that is being compiled.
104105
It will not be populated when the type is only available as a compiled class from a dependency.
105-
For such cases, xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[manual metadata] should be provided.
106+
For such cases, you can xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.source[source the metadata] or xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[provide manual entries].
106107

107108
NOTE: You should only use plain text with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] field Javadoc, since they are not processed before being added to the JSON.
108109

@@ -156,17 +157,37 @@ TIP: This has no effect on collections and maps, as those types are automaticall
156157

157158

158159

160+
[[appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.source]]
161+
=== Configuration Properties Source
162+
163+
If a type located in another module is used in a javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation]-annotated type, some metadata elements cannot be discovered automatically.
164+
Reusing the example above, if `Host` is located in another module, full metadata is not available as the annotation processor does not have access to the source of `Host`.
165+
166+
To handle this use case, add the annotation processor in the module that contains the `Host` type and annotate it with javadoc:org.springframework.boot.context.properties.ConfigurationPropertiesSource[format=annotation]:
167+
168+
include-code::Host[]
169+
170+
This generates the metadata for `Host` in `META-INF/spring/configuration-metadata/com.example.Host.json` and is reused automatically by the annotation processor when it handles such type.
171+
172+
You can also annotate a parent class located in another module that a javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation]-annotated type extends from.
173+
174+
TIP: If you need to reuse metadata for a type that you do not control, create a file named with the pattern above and it will be used as long as it is available on the classpath.
175+
176+
177+
159178
[[appendix.configuration-metadata.annotation-processor.adding-additional-metadata]]
160179
== Adding Additional Metadata
161180

162181
Spring Boot's configuration file handling is quite flexible, and it is often the case that properties may exist that are not bound to a javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] bean.
163182
You may also need to tune some attributes of an existing key or to ignore the key altogether.
164183
To support such cases and let you provide custom "hints", the annotation processor automatically merges items from `META-INF/additional-spring-configuration-metadata.json` into the main metadata file.
165184

185+
When generating source metadata for a type, you can also craft custom metadata for that type, for example `com.example.SomeType`, in `META-INF/spring/configuration/metadata/com.example.SomeType.json`.
186+
166187
If you refer to a property that has been detected automatically, the description, default value, and deprecation information are overridden, if specified.
167188
If the manual property declaration is not identified in the current module, it is added as a new property.
168189

169-
The format of the `additional-spring-configuration-metadata.json` file is exactly the same as the regular `spring-configuration-metadata.json`.
190+
The format of the additional metadata file is exactly the same as the regular `spring-configuration-metadata.json`.
170191
The items contained in the "`ignored.properties`" section are removed from the "`properties`" section of the generated `spring-configuration-metadata.json` file.
171192

172193
The additional properties file is optional.

spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/index.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Spring Boot jars include metadata files that provide details of all supported co
66
The files are designed to let IDE developers offer contextual help and "`code completion`" as users are working with `application.properties` or `application.yaml` files.
77

88
The majority of the metadata file is generated automatically at compile time by processing all items annotated with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation].
9-
However, it is possible to xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[write part of the metadata manually] for corner cases or more advanced use cases.
9+
For corner cases or more advanced use cases, it is possible to xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.source[source the metadata of external types ] or xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[write part of the metadata manually].

spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/manual-hints.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ In order to offer additional content assistance for the keys, you could add the
4242
]}
4343
----
4444

45+
NOTE: Hints can also be added for xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.source[external types] and are applied whenever that type is used.
46+
4547
TIP: We recommend that you use an javadoc:java.lang.Enum[] for those two values instead.
4648
If your IDE supports it, this is by far the most effective approach to auto-completion.
4749

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2012-2025 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+
17+
package org.springframework.boot.docs.appendix.configurationmetadata.annotationprocessor.automaticmetadatageneration.source;
18+
19+
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
20+
21+
@ConfigurationPropertiesSource
22+
public class Host {
23+
24+
/**
25+
* IP address to listen to.
26+
*/
27+
private String ip = "127.0.0.1";
28+
29+
/**
30+
* Port to listener to.
31+
*/
32+
private int port = 9797;
33+
34+
// @fold:on // getters/setters ...
35+
public String getIp() {
36+
return this.ip;
37+
}
38+
39+
public void setIp(String ip) {
40+
this.ip = ip;
41+
}
42+
43+
public int getPort() {
44+
return this.port;
45+
}
46+
47+
public void setPort(int port) {
48+
this.port = port;
49+
}
50+
// @fold:off
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.springframework.boot.docs.configurationmetadata.annotationprocessor.automaticmetadatageneration.source
2+
3+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
4+
5+
@ConfigurationPropertiesScan
6+
class Host {
7+
8+
/**
9+
* IP address to listen to.
10+
*/
11+
var ip: String = "127.0.0.1"
12+
13+
/**
14+
* Port to listener to.
15+
*/
16+
var port = 9797
17+
18+
19+
}

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

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.configurationprocessor;
1818

19-
import java.io.FileNotFoundException;
2019
import java.io.PrintWriter;
2120
import java.io.StringWriter;
2221
import java.time.Duration;
@@ -28,6 +27,7 @@
2827
import java.util.Map;
2928
import java.util.Objects;
3029
import java.util.Set;
30+
import java.util.function.Supplier;
3131

3232
import javax.annotation.processing.AbstractProcessor;
3333
import javax.annotation.processing.ProcessingEnvironment;
@@ -48,6 +48,7 @@
4848
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
4949
import org.springframework.boot.configurationprocessor.metadata.InvalidConfigurationMetadataException;
5050
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
51+
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
5152
import org.springframework.boot.configurationprocessor.metadata.ItemIgnore;
5253
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
5354

@@ -64,6 +65,7 @@
6465
* @since 1.2.0
6566
*/
6667
@SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION,
68+
ConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION,
6769
ConfigurationMetadataAnnotationProcessor.AUTO_CONFIGURATION_ANNOTATION,
6870
ConfigurationMetadataAnnotationProcessor.CONFIGURATION_ANNOTATION,
6971
ConfigurationMetadataAnnotationProcessor.CONTROLLER_ENDPOINT_ANNOTATION,
@@ -78,6 +80,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
7880

7981
static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties";
8082

83+
static final String CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationPropertiesSource";
84+
8185
static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.NestedConfigurationProperty";
8286

8387
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty";
@@ -116,6 +120,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
116120

117121
private MetadataStore metadataStore;
118122

123+
private MetadataCollectors metadataCollectors;
124+
119125
private MetadataCollector metadataCollector;
120126

121127
private MetadataGenerationEnvironment metadataEnv;
@@ -124,6 +130,10 @@ protected String configurationPropertiesAnnotation() {
124130
return CONFIGURATION_PROPERTIES_ANNOTATION;
125131
}
126132

133+
protected String configurationPropertiesSourceAnnotation() {
134+
return CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION;
135+
}
136+
127137
protected String nestedConfigurationPropertyAnnotation() {
128138
return NESTED_CONFIGURATION_PROPERTY_ANNOTATION;
129139
}
@@ -178,23 +188,35 @@ public Set<String> getSupportedOptions() {
178188
@Override
179189
public synchronized void init(ProcessingEnvironment env) {
180190
super.init(env);
181-
this.metadataStore = new MetadataStore(env);
182-
this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata());
191+
TypeUtils typeUtils = new TypeUtils(env);
192+
this.metadataStore = new MetadataStore(env, typeUtils);
193+
this.metadataCollectors = new MetadataCollectors(env, typeUtils);
194+
this.metadataCollector = this.metadataCollectors.getModuleMetadataCollector();
183195
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
184-
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
185-
constructorBindingAnnotation(), autowiredAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
186-
readOperationAnnotation(), optionalParameterAnnotation(), nameAnnotation());
196+
configurationPropertiesSourceAnnotation(), nestedConfigurationPropertyAnnotation(),
197+
deprecatedConfigurationPropertyAnnotation(), constructorBindingAnnotation(), autowiredAnnotation(),
198+
defaultValueAnnotation(), endpointAnnotations(), readOperationAnnotation(),
199+
optionalParameterAnnotation(), nameAnnotation());
187200
}
188201

189202
@Override
190203
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
191-
this.metadataCollector.processing(roundEnv);
204+
this.metadataCollectors.processing(roundEnv);
192205
TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement();
193206
if (annotationType != null) { // Is @ConfigurationProperties available
194207
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
195208
processElement(element);
196209
}
197210
}
211+
TypeElement sourceAnnotationType = this.metadataEnv.getConfigurationPropertiesSourceAnnotationElement();
212+
if (sourceAnnotationType != null) { // Is @ConfigurationPropertiesSource available
213+
for (Element element : roundEnv.getElementsAnnotatedWith(sourceAnnotationType)) {
214+
if (element instanceof TypeElement typeElement) {
215+
MetadataCollector metadataCollector = this.metadataCollectors.getMetadataCollector(typeElement);
216+
processSourceElement(metadataCollector, "", typeElement);
217+
}
218+
}
219+
}
198220
Set<TypeElement> endpointTypes = this.metadataEnv.getEndpointAnnotationElements();
199221
if (!endpointTypes.isEmpty()) { // Are endpoint annotations available
200222
for (TypeElement endpointType : endpointTypes) {
@@ -203,6 +225,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
203225
}
204226
if (roundEnv.processingOver()) {
205227
try {
228+
writeSourceMetadata();
206229
writeMetadata();
207230
}
208231
catch (Exception ex) {
@@ -276,6 +299,10 @@ private void processTypeElement(String prefix, TypeElement element, ExecutableEl
276299
seen.push(element);
277300
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> {
278301
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
302+
ItemHint itemHint = descriptor.resolveItemHint(prefix, this.metadataEnv);
303+
if (itemHint != null) {
304+
this.metadataCollector.add(itemHint);
305+
}
279306
if (descriptor.isNested(this.metadataEnv)) {
280307
TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils()
281308
.asElement(descriptor.getType());
@@ -287,6 +314,18 @@ private void processTypeElement(String prefix, TypeElement element, ExecutableEl
287314
}
288315
}
289316

317+
private void processSourceElement(MetadataCollector metadataCollector, String prefix, TypeElement element) {
318+
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, null).forEach((descriptor) -> {
319+
metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
320+
if (descriptor.isNested(this.metadataEnv)) {
321+
TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils()
322+
.asElement(descriptor.getType());
323+
String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, descriptor.getName());
324+
processSourceElement(metadataCollector, nestedPrefix, nestedTypeElement);
325+
}
326+
});
327+
}
328+
290329
private void processEndpoint(Element element, List<Element> annotations) {
291330
try {
292331
String annotationName = this.metadataEnv.getTypeUtils().getQualifiedName(annotations.get(0));
@@ -381,9 +420,20 @@ private String getPrefix(AnnotationMirror annotation) {
381420
return this.metadataEnv.getAnnotationElementStringValue(annotation, "value");
382421
}
383422

423+
protected void writeSourceMetadata() throws Exception {
424+
for (TypeElement sourceType : this.metadataCollectors.getSourceTypes()) {
425+
ConfigurationMetadata metadata = this.metadataCollectors.getMetadataCollector(sourceType).getMetadata();
426+
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata(sourceType));
427+
removeIgnored(metadata);
428+
if (!metadata.getItems().isEmpty()) {
429+
this.metadataStore.writeMetadata(metadata, sourceType);
430+
}
431+
}
432+
}
433+
384434
protected ConfigurationMetadata writeMetadata() throws Exception {
385435
ConfigurationMetadata metadata = this.metadataCollector.getMetadata();
386-
metadata = mergeAdditionalMetadata(metadata);
436+
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata());
387437
removeIgnored(metadata);
388438
if (!metadata.getItems().isEmpty()) {
389439
this.metadataStore.writeMetadata(metadata);
@@ -398,14 +448,16 @@ private void removeIgnored(ConfigurationMetadata metadata) {
398448
}
399449
}
400450

401-
private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) {
451+
private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata,
452+
Supplier<ConfigurationMetadata> additionalMetadataSupplier) {
402453
try {
403-
ConfigurationMetadata merged = new ConfigurationMetadata(metadata);
404-
merged.merge(this.metadataStore.readAdditionalMetadata());
405-
return merged;
406-
}
407-
catch (FileNotFoundException ex) {
408-
// No additional metadata
454+
ConfigurationMetadata additionalMetadata = additionalMetadataSupplier.get();
455+
if (additionalMetadata != null) {
456+
ConfigurationMetadata merged = new ConfigurationMetadata(metadata);
457+
merged.merge(additionalMetadata);
458+
return merged;
459+
}
460+
return metadata;
409461
}
410462
catch (InvalidConfigurationMetadataException ex) {
411463
log(ex.getKind(), ex.getMessage());

0 commit comments

Comments
 (0)