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
This commit is contained in:
Stéphane Nicoll
2025-05-05 10:42:47 +02:00
parent 2fa8d38aa7
commit b3f35baed3
55 changed files with 2548 additions and 139 deletions

View File

@@ -84,9 +84,9 @@ With Gradle, declare the dependencies in the `annotationProcessor` configuration
[[appendix.configuration-metadata.annotation-processor.automatic-metadata-generation]]
== Automatic Metadata Generation
The processor picks up both classes and methods that are annotated with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation].
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]
NOTE: Custom annotations that are meta-annotated with javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] are not supported.
NOTE: Custom annotations that are meta-annotated with either of those annotations are not supported.
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].
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[]
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.
The Javadoc on fields is used to populate the `description` attribute.
For instance, the description of `my.server.ip` is "IP address to listen to.".
The `description` attribute can only be populated when the type is available as source code that is being compiled.
It will not be populated when the type is only available as a compiled class from a dependency.
For such cases, xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[manual metadata] should be provided.
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].
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.
@@ -156,6 +157,24 @@ TIP: This has no effect on collections and maps, as those types are automaticall
[[appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.source]]
=== Configuration Properties Source
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.
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`.
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]:
include-code::Host[]
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.
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.
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.
[[appendix.configuration-metadata.annotation-processor.adding-additional-metadata]]
== Adding Additional Metadata
@@ -163,10 +182,12 @@ Spring Boot's configuration file handling is quite flexible, and it is often the
You may also need to tune some attributes of an existing key or to ignore the key altogether.
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.
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`.
If you refer to a property that has been detected automatically, the description, default value, and deprecation information are overridden, if specified.
If the manual property declaration is not identified in the current module, it is added as a new property.
The format of the `additional-spring-configuration-metadata.json` file is exactly the same as the regular `spring-configuration-metadata.json`.
The format of the additional metadata file is exactly the same as the regular `spring-configuration-metadata.json`.
The items contained in the "`ignored.properties`" section are removed from the "`properties`" section of the generated `spring-configuration-metadata.json` file.
The additional properties file is optional.

View File

@@ -6,4 +6,4 @@ Spring Boot jars include metadata files that provide details of all supported co
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.
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].
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.
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].

View File

@@ -42,6 +42,8 @@ In order to offer additional content assistance for the keys, you could add the
]}
----
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.
TIP: We recommend that you use an javadoc:java.lang.Enum[] for those two values instead.
If your IDE supports it, this is by far the most effective approach to auto-completion.

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2025 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.docs.appendix.configurationmetadata.annotationprocessor.automaticmetadatageneration.source;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
@ConfigurationPropertiesSource
public class Host {
/**
* IP address to listen to.
*/
private String ip = "127.0.0.1";
/**
* Port to listener to.
*/
private int port = 9797;
// @fold:on // getters/setters ...
public String getIp() {
return this.ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
// @fold:off
}

View File

@@ -0,0 +1,19 @@
package org.springframework.boot.docs.configurationmetadata.annotationprocessor.automaticmetadatageneration.source
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
@ConfigurationPropertiesScan
class Host {
/**
* IP address to listen to.
*/
var ip: String = "127.0.0.1"
/**
* Port to listener to.
*/
var port = 9797
}

View File

@@ -16,7 +16,6 @@
package org.springframework.boot.configurationprocessor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
@@ -28,6 +27,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
@@ -48,6 +48,7 @@ import javax.tools.Diagnostic.Kind;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.InvalidConfigurationMetadataException;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemIgnore;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
@@ -64,6 +65,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
* @since 1.2.0
*/
@SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION,
ConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION,
ConfigurationMetadataAnnotationProcessor.AUTO_CONFIGURATION_ANNOTATION,
ConfigurationMetadataAnnotationProcessor.CONFIGURATION_ANNOTATION,
ConfigurationMetadataAnnotationProcessor.CONTROLLER_ENDPOINT_ANNOTATION,
@@ -78,6 +80,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties";
static final String CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationPropertiesSource";
static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.NestedConfigurationProperty";
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty";
@@ -116,6 +120,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
private MetadataStore metadataStore;
private MetadataCollectors metadataCollectors;
private MetadataCollector metadataCollector;
private MetadataGenerationEnvironment metadataEnv;
@@ -124,6 +130,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return CONFIGURATION_PROPERTIES_ANNOTATION;
}
protected String configurationPropertiesSourceAnnotation() {
return CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION;
}
protected String nestedConfigurationPropertyAnnotation() {
return NESTED_CONFIGURATION_PROPERTY_ANNOTATION;
}
@@ -178,23 +188,35 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
this.metadataStore = new MetadataStore(env);
this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata());
TypeUtils typeUtils = new TypeUtils(env);
this.metadataStore = new MetadataStore(env, typeUtils);
this.metadataCollectors = new MetadataCollectors(env, typeUtils);
this.metadataCollector = this.metadataCollectors.getModuleMetadataCollector();
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
constructorBindingAnnotation(), autowiredAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
readOperationAnnotation(), optionalParameterAnnotation(), nameAnnotation());
configurationPropertiesSourceAnnotation(), nestedConfigurationPropertyAnnotation(),
deprecatedConfigurationPropertyAnnotation(), constructorBindingAnnotation(), autowiredAnnotation(),
defaultValueAnnotation(), endpointAnnotations(), readOperationAnnotation(),
optionalParameterAnnotation(), nameAnnotation());
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
this.metadataCollector.processing(roundEnv);
this.metadataCollectors.processing(roundEnv);
TypeElement annotationType = this.metadataEnv.getConfigurationPropertiesAnnotationElement();
if (annotationType != null) { // Is @ConfigurationProperties available
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
processElement(element);
}
}
TypeElement sourceAnnotationType = this.metadataEnv.getConfigurationPropertiesSourceAnnotationElement();
if (sourceAnnotationType != null) { // Is @ConfigurationPropertiesSource available
for (Element element : roundEnv.getElementsAnnotatedWith(sourceAnnotationType)) {
if (element instanceof TypeElement typeElement) {
MetadataCollector metadataCollector = this.metadataCollectors.getMetadataCollector(typeElement);
processSourceElement(metadataCollector, "", typeElement);
}
}
}
Set<TypeElement> endpointTypes = this.metadataEnv.getEndpointAnnotationElements();
if (!endpointTypes.isEmpty()) { // Are endpoint annotations available
for (TypeElement endpointType : endpointTypes) {
@@ -203,6 +225,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
}
if (roundEnv.processingOver()) {
try {
writeSourceMetadata();
writeMetadata();
}
catch (Exception ex) {
@@ -276,6 +299,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
seen.push(element);
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> {
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
ItemHint itemHint = descriptor.resolveItemHint(prefix, this.metadataEnv);
if (itemHint != null) {
this.metadataCollector.add(itemHint);
}
if (descriptor.isNested(this.metadataEnv)) {
TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils()
.asElement(descriptor.getType());
@@ -287,6 +314,18 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
}
}
private void processSourceElement(MetadataCollector metadataCollector, String prefix, TypeElement element) {
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, null).forEach((descriptor) -> {
metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
if (descriptor.isNested(this.metadataEnv)) {
TypeElement nestedTypeElement = (TypeElement) this.metadataEnv.getTypeUtils()
.asElement(descriptor.getType());
String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, descriptor.getName());
processSourceElement(metadataCollector, nestedPrefix, nestedTypeElement);
}
});
}
private void processEndpoint(Element element, List<Element> annotations) {
try {
String annotationName = this.metadataEnv.getTypeUtils().getQualifiedName(annotations.get(0));
@@ -381,9 +420,20 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return this.metadataEnv.getAnnotationElementStringValue(annotation, "value");
}
protected void writeSourceMetadata() throws Exception {
for (TypeElement sourceType : this.metadataCollectors.getSourceTypes()) {
ConfigurationMetadata metadata = this.metadataCollectors.getMetadataCollector(sourceType).getMetadata();
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata(sourceType));
removeIgnored(metadata);
if (!metadata.getItems().isEmpty()) {
this.metadataStore.writeMetadata(metadata, sourceType);
}
}
}
protected ConfigurationMetadata writeMetadata() throws Exception {
ConfigurationMetadata metadata = this.metadataCollector.getMetadata();
metadata = mergeAdditionalMetadata(metadata);
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata());
removeIgnored(metadata);
if (!metadata.getItems().isEmpty()) {
this.metadataStore.writeMetadata(metadata);
@@ -398,14 +448,16 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
}
}
private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) {
private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata,
Supplier<ConfigurationMetadata> additionalMetadataSupplier) {
try {
ConfigurationMetadata merged = new ConfigurationMetadata(metadata);
merged.merge(this.metadataStore.readAdditionalMetadata());
return merged;
}
catch (FileNotFoundException ex) {
// No additional metadata
ConfigurationMetadata additionalMetadata = additionalMetadataSupplier.get();
if (additionalMetadata != null) {
ConfigurationMetadata merged = new ConfigurationMetadata(metadata);
merged.merge(additionalMetadata);
return merged;
}
return metadata;
}
catch (InvalidConfigurationMetadataException ex) {
log(ex.getKind(), ex.getMessage());

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2012-2025 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.configurationprocessor;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
/**
* Resolve source configuration metadata for arbitrary types.
*
* @author Stephane Nicoll
*/
class ConfigurationPropertiesSourceResolver {
private final ProcessingEnvironment processingEnvironment;
private final TypeUtils typeUtils;
ConfigurationPropertiesSourceResolver(ProcessingEnvironment processingEnvironment, TypeUtils typeUtils) {
this.typeUtils = typeUtils;
this.processingEnvironment = processingEnvironment;
}
/**
* Resolve the {@link SourceMetadata} for the specified type. If the type has no
* source metadata, return an {@link SourceMetadata#EMPTY} source.
* @param typeElement the type to discover source metadata from
* @return the source metadata for the specified type
*/
SourceMetadata resolveSource(TypeElement typeElement) {
ConfigurationMetadata configurationMetadata = resolveConfigurationMetadata(typeElement);
return (configurationMetadata != null)
? new SourceMetadata(configurationMetadata.getItems(), configurationMetadata.getHints())
: SourceMetadata.EMPTY;
}
private ConfigurationMetadata resolveConfigurationMetadata(TypeElement type) {
try {
String sourceLocation = MetadataStore.SOURCE_METADATA_PATH.apply(type, this.typeUtils);
FileObject resource = this.processingEnvironment.getFiler()
.getResource(StandardLocation.CLASS_PATH, "", sourceLocation);
return (resource != null) ? new JsonMarshaller().read(resource.openInputStream()) : null;
}
catch (Exception ex) {
return null;
}
}
/**
* Additional source of metadata.
*/
static final class SourceMetadata {
/**
* An empty source metadata.
*/
public static final SourceMetadata EMPTY = new SourceMetadata(Collections.emptyList(), Collections.emptyList());
private final Map<String, ItemMetadata> items;
private final Map<String, ItemHint> hints;
private SourceMetadata(List<ItemMetadata> items, List<ItemHint> hints) {
this.items = items.stream()
.collect(Collectors.toMap((item) -> ConventionUtils.toDashedCase(item.getName()), Function.identity()));
this.hints = hints.stream()
.collect(Collectors.toMap((item) -> ConventionUtils.toDashedCase(item.getName()), Function.identity()));
}
/**
* Create a {@link PropertyDescriptor} for the given property name.
* @param name the name of a property
* @param propertyDescriptor the descriptor of the property
* @return a property descriptor that applies additional source metadata if
* necessary
*/
PropertyDescriptor createPropertyDescriptor(String name, PropertyDescriptor propertyDescriptor) {
String key = ConventionUtils.toDashedCase(name);
if (this.items.containsKey(key)) {
ItemMetadata itemMetadata = this.items.get(key);
ItemHint itemHint = this.hints.get(key);
return new SourcePropertyDescriptor(propertyDescriptor, itemMetadata, itemHint);
}
return propertyDescriptor;
}
/**
* Create a {@link PropertyDescriptor} for the given property name.
* @param name the name of a property
* @param regularDescriptor a function to get the descriptor
* @return a property descriptor that applies additional source metadata if
* necessary
*/
PropertyDescriptor createPropertyDescriptor(String name,
Function<String, PropertyDescriptor> regularDescriptor) {
return createPropertyDescriptor(name, regularDescriptor.apply(name));
}
}
/**
* A {@link PropertyDescriptor} that applies source metadata.
*/
static class SourcePropertyDescriptor extends PropertyDescriptor {
private final PropertyDescriptor delegate;
private final ItemMetadata sourceItemMetadata;
private final ItemHint sourceItemHint;
SourcePropertyDescriptor(PropertyDescriptor delegate, ItemMetadata sourceItemMetadata,
ItemHint sourceItemHint) {
super(delegate.getName(), delegate.getType(), delegate.getDeclaringElement(), delegate.getGetter());
this.delegate = delegate;
this.sourceItemMetadata = sourceItemMetadata;
this.sourceItemHint = sourceItemHint;
}
@Override
protected ItemHint resolveItemHint(String prefix, MetadataGenerationEnvironment environment) {
return (this.sourceItemHint != null) ? this.sourceItemHint.applyPrefix(prefix)
: super.resolveItemHint(prefix, environment);
}
@Override
protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) {
return this.delegate.isMarkedAsNested(environment);
}
@Override
protected String resolveDescription(MetadataGenerationEnvironment environment) {
String description = this.delegate.resolveDescription(environment);
return (description != null) ? description : this.sourceItemMetadata.getDescription();
}
@Override
protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) {
Object defaultValue = this.delegate.resolveDefaultValue(environment);
return (defaultValue != null) ? defaultValue : this.sourceItemMetadata.getDefaultValue();
}
@Override
protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) {
ItemDeprecation itemDeprecation = this.delegate.resolveItemDeprecation(environment);
return (itemDeprecation != null) ? itemDeprecation : this.sourceItemMetadata.getDeprecation();
}
@Override
boolean isProperty(MetadataGenerationEnvironment environment) {
return this.delegate.isProperty(environment);
}
}
}

View File

@@ -16,15 +16,13 @@
package org.springframework.boot.configurationprocessor;
import java.util.Arrays;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
/**
* A {@link PropertyDescriptor} for a constructor parameter.
*
@@ -45,8 +43,8 @@ class ConstructorParameterPropertyDescriptor extends ParameterPropertyDescriptor
}
@Override
protected List<Element> getDeprecatableElements() {
return Arrays.asList(getGetter(), this.setter, this.field);
protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) {
return resolveItemDeprecation(environment, getGetter(), this.setter, this.field);
}
@Override

View File

@@ -16,15 +16,13 @@
package org.springframework.boot.configurationprocessor;
import java.util.Arrays;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
/**
* A {@link PropertyDescriptor} for a standard JavaBean property.
*
@@ -68,8 +66,8 @@ class JavaBeanPropertyDescriptor extends PropertyDescriptor {
}
@Override
protected List<Element> getDeprecatableElements() {
return Arrays.asList(getGetter(), this.setter, this.field, this.factoryMethod);
protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) {
return resolveItemDeprecation(environment, getGetter(), this.setter, this.field, this.factoryMethod);
}
@Override

View File

@@ -16,18 +16,17 @@
package org.springframework.boot.configurationprocessor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
/**
* A {@link PropertyDescriptor} for a Lombok field.
*
@@ -80,8 +79,8 @@ class LombokPropertyDescriptor extends PropertyDescriptor {
}
@Override
protected List<Element> getDeprecatableElements() {
return Arrays.asList(getGetter(), this.setter, this.field, this.factoryMethod);
protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) {
return resolveItemDeprecation(environment, getGetter(), this.setter, this.field, this.factoryMethod);
}
@Override

View File

@@ -16,18 +16,14 @@
package org.springframework.boot.configurationprocessor;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.function.Predicate;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
@@ -37,48 +33,33 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
* @author Andy Wilkinson
* @author Kris De Volder
* @author Moritz Halbritter
* @since 1.2.2
* @author Stephane Nicoll
*/
public class MetadataCollector {
class MetadataCollector {
private final Set<ItemMetadata> metadataItems = new LinkedHashSet<>();
private final ProcessingEnvironment processingEnvironment;
private final Predicate<ItemMetadata> mergeRequired;
private final ConfigurationMetadata previousMetadata;
private final TypeUtils typeUtils;
private final Set<ItemMetadata> metadataItems = new LinkedHashSet<>();
private final Set<String> processedSourceTypes = new HashSet<>();
private final Set<ItemHint> metadataHints = new LinkedHashSet<>();
/**
* Creates a new {@code MetadataProcessor} instance.
* @param processingEnvironment the processing environment of the build
* @param mergeRequired specify whether an item can be merged
* @param previousMetadata any previous metadata or {@code null}
*/
public MetadataCollector(ProcessingEnvironment processingEnvironment, ConfigurationMetadata previousMetadata) {
this.processingEnvironment = processingEnvironment;
MetadataCollector(Predicate<ItemMetadata> mergeRequired, ConfigurationMetadata previousMetadata) {
this.mergeRequired = mergeRequired;
this.previousMetadata = previousMetadata;
this.typeUtils = new TypeUtils(processingEnvironment);
}
public void processing(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getRootElements()) {
markAsProcessed(element);
}
}
private void markAsProcessed(Element element) {
if (element instanceof TypeElement) {
this.processedSourceTypes.add(this.typeUtils.getQualifiedName(element));
}
}
public void add(ItemMetadata metadata) {
void add(ItemMetadata metadata) {
this.metadataItems.add(metadata);
}
public void add(ItemMetadata metadata, Consumer<ItemMetadata> onConflict) {
void add(ItemMetadata metadata, Consumer<ItemMetadata> onConflict) {
ItemMetadata existing = find(metadata.getName());
if (existing != null) {
onConflict.accept(existing);
@@ -87,7 +68,7 @@ public class MetadataCollector {
add(metadata);
}
public boolean addIfAbsent(ItemMetadata metadata) {
boolean addIfAbsent(ItemMetadata metadata) {
ItemMetadata existing = find(metadata.getName());
if (existing != null) {
return false;
@@ -96,7 +77,11 @@ public class MetadataCollector {
return true;
}
public boolean hasSimilarGroup(ItemMetadata metadata) {
void add(ItemHint itemHint) {
this.metadataHints.add(itemHint);
}
boolean hasSimilarGroup(ItemMetadata metadata) {
if (!metadata.isOfItemType(ItemMetadata.ItemType.GROUP)) {
throw new IllegalStateException("item " + metadata + " must be a group");
}
@@ -109,15 +94,18 @@ public class MetadataCollector {
return false;
}
public ConfigurationMetadata getMetadata() {
ConfigurationMetadata getMetadata() {
ConfigurationMetadata metadata = new ConfigurationMetadata();
for (ItemMetadata item : this.metadataItems) {
metadata.add(item);
}
for (ItemHint metadataHint : this.metadataHints) {
metadata.add(metadataHint);
}
if (this.previousMetadata != null) {
List<ItemMetadata> items = this.previousMetadata.getItems();
for (ItemMetadata item : items) {
if (shouldBeMerged(item)) {
if (this.mergeRequired.test(item)) {
metadata.addIfMissing(item);
}
}
@@ -132,17 +120,4 @@ public class MetadataCollector {
.orElse(null);
}
private boolean shouldBeMerged(ItemMetadata itemMetadata) {
String sourceType = itemMetadata.getSourceType();
return (sourceType != null && !deletedInCurrentBuild(sourceType) && !processedInCurrentBuild(sourceType));
}
private boolean deletedInCurrentBuild(String sourceType) {
return this.processingEnvironment.getElementUtils().getTypeElement(sourceType.replace('$', '.')) == null;
}
private boolean processedInCurrentBuild(String sourceType) {
return this.processedSourceTypes.contains(sourceType);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2025 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.configurationprocessor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
* Container for {@link MetadataCollector}. Usually, either metadata for the whole module
* or metadata for types is generated. This makes sure to record types that have been
* processed and determine if previous metadata should be merged.
*
* @author Stephane Nicoll
*/
class MetadataCollectors {
private final ProcessingEnvironment processingEnvironment;
private final TypeUtils typeUtils;
private final MetadataStore metadataStore;
private final MetadataCollector metadataCollector;
private final Set<String> processedSourceTypes = new HashSet<>();
private final Map<TypeElement, MetadataCollector> metadataTypeCollectors = new HashMap<>();
MetadataCollectors(ProcessingEnvironment processingEnvironment, TypeUtils typeUtils) {
this.processingEnvironment = processingEnvironment;
this.typeUtils = typeUtils;
this.metadataStore = new MetadataStore(this.processingEnvironment, this.typeUtils);
this.metadataCollector = new MetadataCollector(this::shouldBeMerged, this.metadataStore.readMetadata());
}
void processing(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getRootElements()) {
if (element instanceof TypeElement) {
this.processedSourceTypes.add(this.typeUtils.getQualifiedName(element));
}
}
}
MetadataCollector getModuleMetadataCollector() {
return this.metadataCollector;
}
MetadataCollector getMetadataCollector(TypeElement element) {
return this.metadataTypeCollectors.computeIfAbsent(element,
(ignored) -> new MetadataCollector(this::shouldBeMerged, this.metadataStore.readMetadata(element)));
}
Set<TypeElement> getSourceTypes() {
return this.metadataTypeCollectors.keySet();
}
private boolean shouldBeMerged(ItemMetadata itemMetadata) {
String sourceType = itemMetadata.getSourceType();
return (sourceType != null && !deletedInCurrentBuild(sourceType) && !processedInCurrentBuild(sourceType));
}
private boolean deletedInCurrentBuild(String sourceType) {
return this.processingEnvironment.getElementUtils().getTypeElement(sourceType.replace('$', '.')) == null;
}
private boolean processedInCurrentBuild(String sourceType) {
return this.processedSourceTypes.contains(sourceType);
}
}

View File

@@ -41,6 +41,7 @@ import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import org.springframework.boot.configurationprocessor.ConfigurationPropertiesSourceResolver.SourceMetadata;
import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
@@ -75,12 +76,18 @@ class MetadataGenerationEnvironment {
private final FieldValuesParser fieldValuesParser;
private final ConfigurationPropertiesSourceResolver sourceResolver;
private final Map<TypeElement, Map<String, Object>> defaultValues = new HashMap<>();
private final Map<TypeElement, SourceMetadata> sources = new HashMap<>();
private final String configurationPropertiesAnnotation;
private final String nestedConfigurationPropertyAnnotation;
private final String configurationPropertiesSourceAnnotation;
private final String deprecatedConfigurationPropertyAnnotation;
private final String constructorBindingAnnotation;
@@ -98,15 +105,17 @@ class MetadataGenerationEnvironment {
private final String autowiredAnnotation;
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
String constructorBindingAnnotation, String autowiredAnnotation, String defaultValueAnnotation,
Set<String> endpointAnnotations, String readOperationAnnotation, String optionalParameterAnnotation,
String nameAnnotation) {
String configurationPropertiesSourceAnnotation, String nestedConfigurationPropertyAnnotation,
String deprecatedConfigurationPropertyAnnotation, String constructorBindingAnnotation,
String autowiredAnnotation, String defaultValueAnnotation, Set<String> endpointAnnotations,
String readOperationAnnotation, String optionalParameterAnnotation, String nameAnnotation) {
this.typeUtils = new TypeUtils(environment);
this.elements = environment.getElementUtils();
this.messager = environment.getMessager();
this.fieldValuesParser = resolveFieldValuesParser(environment);
this.sourceResolver = new ConfigurationPropertiesSourceResolver(environment, this.typeUtils);
this.configurationPropertiesAnnotation = configurationPropertiesAnnotation;
this.configurationPropertiesSourceAnnotation = configurationPropertiesSourceAnnotation;
this.nestedConfigurationPropertyAnnotation = nestedConfigurationPropertyAnnotation;
this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation;
this.constructorBindingAnnotation = constructorBindingAnnotation;
@@ -146,6 +155,22 @@ class MetadataGenerationEnvironment {
return this.defaultValues.computeIfAbsent(type, this::resolveFieldValues).get(name);
}
/**
* Resolve the {@link SourceMetadata} for the specified property.
* @param field the field of the property (can be {@code null})
* @param getter the getter of the property (can be {@code null})
* @return the {@link SourceMetadata} for the specified property
*/
SourceMetadata resolveSourceMetadata(VariableElement field, ExecutableElement getter) {
if (field != null && field.getEnclosingElement() instanceof TypeElement type) {
return this.sources.computeIfAbsent(type, this.sourceResolver::resolveSource);
}
if (getter != null && getter.getEnclosingElement() instanceof TypeElement type) {
return this.sources.computeIfAbsent(type, this.sourceResolver::resolveSource);
}
return SourceMetadata.EMPTY;
}
boolean isExcluded(TypeMirror type) {
if (type == null) {
return false;
@@ -314,6 +339,10 @@ class MetadataGenerationEnvironment {
return getAnnotation(element, this.configurationPropertiesAnnotation);
}
TypeElement getConfigurationPropertiesSourceAnnotationElement() {
return this.elements.getTypeElement(this.configurationPropertiesSourceAnnotation);
}
AnnotationMirror getNestedConfigurationPropertyAnnotation(Element element) {
return getAnnotation(element, this.nestedConfigurationPropertyAnnotation);
}

View File

@@ -22,8 +22,10 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.BiFunction;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
@@ -37,46 +39,123 @@ import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
*
* @author Andy Wilkinson
* @author Scott Frederick
* @since 1.2.2
*/
public class MetadataStore {
class MetadataStore {
static final String METADATA_PATH = "META-INF/spring-configuration-metadata.json";
static final BiFunction<TypeElement, TypeUtils, String> SOURCE_METADATA_PATH = (type,
typeUtils) -> "META-INF/spring/configuration-metadata/%s.json".formatted(typeUtils.getQualifiedName(type));
private static final String ADDITIONAL_METADATA_PATH = "META-INF/additional-spring-configuration-metadata.json";
static final BiFunction<TypeElement, TypeUtils, String> ADDITIONAL_SOURCE_METADATA_PATH = (type,
typeUtils) -> "META-INF/spring/configuration-metadata/additional/%s.json"
.formatted(typeUtils.getQualifiedName(type));
private static final String RESOURCES_DIRECTORY = "resources";
private static final String CLASSES_DIRECTORY = "classes";
private final ProcessingEnvironment environment;
public MetadataStore(ProcessingEnvironment environment) {
private final TypeUtils typeUtils;
MetadataStore(ProcessingEnvironment environment, TypeUtils typeUtils) {
this.environment = environment;
this.typeUtils = typeUtils;
}
public ConfigurationMetadata readMetadata() {
/**
* Read the existing {@link ConfigurationMetadata} of the current module or
* {@code null} if it is not available yet.
* @return the metadata or {@code null} if none is present
*/
ConfigurationMetadata readMetadata() {
return readMetadata(METADATA_PATH);
}
/**
* Read the existing {@link ConfigurationMetadata} for the specified type or
* {@code null} if it is not available yet.
* @param typeElement the type to read metadata for
* @return the metadata for the given type or {@code null}
*/
ConfigurationMetadata readMetadata(TypeElement typeElement) {
return readMetadata(SOURCE_METADATA_PATH.apply(typeElement, this.typeUtils));
}
private ConfigurationMetadata readMetadata(String location) {
try {
return readMetadata(getMetadataResource().openInputStream());
return readMetadata(location, getMetadataResource(location).openInputStream());
}
catch (IOException ex) {
return null;
}
}
public void writeMetadata(ConfigurationMetadata metadata) throws IOException {
/**
* Write the module {@link ConfigurationMetadata} to the filesystem.
* @param metadata the metadata to write
*/
void writeMetadata(ConfigurationMetadata metadata) throws IOException {
writeMetadata(metadata, () -> createMetadataResource(METADATA_PATH));
}
/**
* Write the {@link ConfigurationMetadata} for the {@link TypeElement} to the
* filesystem.
* @param metadata the metadata to write
* @param typeElement the type to write metadata for
*/
void writeMetadata(ConfigurationMetadata metadata, TypeElement typeElement) throws IOException {
writeMetadata(metadata, () -> createMetadataResource(SOURCE_METADATA_PATH.apply(typeElement, this.typeUtils)));
}
/**
* Write the metadata to the {@link FileObject} provided by the given supplier.
* @param metadata the metadata to provide
* @param fileObjectProvider a supplier for the {@link FileObject} to use
*/
private void writeMetadata(ConfigurationMetadata metadata, FileObjectSupplier fileObjectProvider)
throws IOException {
if (!metadata.getItems().isEmpty()) {
try (OutputStream outputStream = createMetadataResource().openOutputStream()) {
try (OutputStream outputStream = fileObjectProvider.get().openOutputStream()) {
new JsonMarshaller().write(metadata, outputStream);
}
}
}
public ConfigurationMetadata readAdditionalMetadata() throws IOException {
return readMetadata(getAdditionalMetadataStream());
/**
* Read additional {@link ConfigurationMetadata} for the current module or
* {@code null}.
* @return additional metadata or {@code null} if none is present
*/
ConfigurationMetadata readAdditionalMetadata() {
return readAdditionalMetadata(ADDITIONAL_METADATA_PATH);
}
private ConfigurationMetadata readMetadata(InputStream in) {
/**
* Read additional {@link ConfigurationMetadata} for the {@link TypeElement} or
* {@code null}.
* @param typeElement the type to get additional metadata for
* @return additional metadata for the given type or {@code null} if none is present
*/
ConfigurationMetadata readAdditionalMetadata(TypeElement typeElement) {
return readAdditionalMetadata(ADDITIONAL_SOURCE_METADATA_PATH.apply(typeElement, this.typeUtils));
}
private ConfigurationMetadata readAdditionalMetadata(String location) {
try {
InputStream in = getAdditionalMetadataStream(location);
return readMetadata(location, in);
}
catch (IOException ex) {
return null;
}
}
private ConfigurationMetadata readMetadata(String location, InputStream in) {
try (in) {
return new JsonMarshaller().read(in);
}
@@ -85,29 +164,28 @@ public class MetadataStore {
}
catch (Exception ex) {
throw new InvalidConfigurationMetadataException(
"Invalid additional meta-data in '" + METADATA_PATH + "': " + ex.getMessage(),
Diagnostic.Kind.ERROR);
"Invalid additional meta-data in '" + location + "': " + ex.getMessage(), Diagnostic.Kind.ERROR);
}
}
private FileObject getMetadataResource() throws IOException {
return this.environment.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH);
private FileObject getMetadataResource(String location) throws IOException {
return this.environment.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", location);
}
private FileObject createMetadataResource() throws IOException {
return this.environment.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH);
private FileObject createMetadataResource(String location) throws IOException {
return this.environment.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", location);
}
private InputStream getAdditionalMetadataStream() throws IOException {
private InputStream getAdditionalMetadataStream(String additionalMetadataLocation) throws IOException {
// Most build systems will have copied the file to the class output location
FileObject fileObject = this.environment.getFiler()
.getResource(StandardLocation.CLASS_OUTPUT, "", ADDITIONAL_METADATA_PATH);
.getResource(StandardLocation.CLASS_OUTPUT, "", additionalMetadataLocation);
InputStream inputStream = getMetadataStream(fileObject);
if (inputStream != null) {
return inputStream;
}
try {
File file = locateAdditionalMetadataFile(new File(fileObject.toUri()));
File file = locateAdditionalMetadataFile(new File(fileObject.toUri()), additionalMetadataLocation);
return (file.exists() ? new FileInputStream(file) : fileObject.toUri().toURL().openStream());
}
catch (Exception ex) {
@@ -124,7 +202,7 @@ public class MetadataStore {
}
}
File locateAdditionalMetadataFile(File standardLocation) throws IOException {
File locateAdditionalMetadataFile(File standardLocation, String additionalMetadataLocation) throws IOException {
if (standardLocation.exists()) {
return standardLocation;
}
@@ -132,13 +210,13 @@ public class MetadataStore {
.get(ConfigurationMetadataAnnotationProcessor.ADDITIONAL_METADATA_LOCATIONS_OPTION);
if (locations != null) {
for (String location : locations.split(",")) {
File candidate = new File(location, ADDITIONAL_METADATA_PATH);
File candidate = new File(location, additionalMetadataLocation);
if (candidate.isFile()) {
return candidate;
}
}
}
return new File(locateGradleResourcesDirectory(standardLocation), ADDITIONAL_METADATA_PATH);
return new File(locateGradleResourcesDirectory(standardLocation), additionalMetadataLocation);
}
private File locateGradleResourcesDirectory(File standardAdditionalMetadataLocation) throws FileNotFoundException {
@@ -152,4 +230,13 @@ public class MetadataStore {
return new File(buildDirectoryPath, RESOURCES_DIRECTORY + '/' + classOutputLocation.getName());
}
/**
* Internal callback that can throw an {@link IOException}.
*/
private interface FileObjectSupplier {
FileObject get() throws IOException;
}
}

View File

@@ -16,7 +16,7 @@
package org.springframework.boot.configurationprocessor;
import java.util.List;
import java.util.Arrays;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
@@ -26,6 +26,7 @@ import javax.lang.model.type.TypeMirror;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
@@ -106,6 +107,16 @@ abstract class PropertyDescriptor {
return null;
}
/**
* Resolve the {@link ItemHint} for this property.
* @param prefix the property prefix
* @param environment the metadata generation environment
* @return the item hint or {@code null}
*/
protected ItemHint resolveItemHint(String prefix, MetadataGenerationEnvironment environment) {
return null;
}
/**
* Return if this is a nested property.
* @param environment the metadata generation environment
@@ -185,13 +196,14 @@ abstract class PropertyDescriptor {
deprecation);
}
private String resolveType(MetadataGenerationEnvironment environment) {
return environment.getTypeUtils().getType(getDeclaringElement(), getType());
protected final ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment,
Element... elements) {
boolean deprecated = Arrays.stream(elements).anyMatch(environment::isDeprecated);
return deprecated ? environment.resolveItemDeprecation(getGetter()) : null;
}
private ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) {
boolean deprecated = getDeprecatableElements().stream().anyMatch(environment::isDeprecated);
return deprecated ? environment.resolveItemDeprecation(getGetter()) : null;
private String resolveType(MetadataGenerationEnvironment environment) {
return environment.getTypeUtils().getType(getDeclaringElement(), getType());
}
/**
@@ -209,11 +221,11 @@ abstract class PropertyDescriptor {
protected abstract Object resolveDefaultValue(MetadataGenerationEnvironment environment);
/**
* Return all the elements that should be considered when checking for deprecation
* annotations.
* @return the deprecatable elements
* Resolve the {@link ItemDeprecation} for this property.
* @param environment the metadata generation environment
* @return the deprecation or {@code null}
*/
protected abstract List<Element> getDeprecatableElements();
protected abstract ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment);
/**
* Return true if this descriptor is for a property.

View File

@@ -32,6 +32,8 @@ import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.springframework.boot.configurationprocessor.ConfigurationPropertiesSourceResolver.SourceMetadata;
/**
* Resolve {@link PropertyDescriptor} instances.
*
@@ -91,11 +93,13 @@ class PropertyDescriptorResolver {
ExecutableElement setter = members.getPublicSetter(name, type);
VariableElement field = members.getFields().get(name);
RecordComponentElement recordComponent = members.getRecordComponents().get(name);
return (recordComponent != null)
SourceMetadata sourceMetadata = this.environment.resolveSourceMetadata(field, getter);
PropertyDescriptor propertyDescriptor = (recordComponent != null)
? new RecordParameterPropertyDescriptor(name, type, parameter, declaringElement, getter,
recordComponent)
: new ConstructorParameterPropertyDescriptor(name, type, parameter, declaringElement, getter, setter,
field);
return sourceMetadata.createPropertyDescriptor(name, propertyDescriptor);
}
private String getPropertyName(VariableElement parameter) {
@@ -118,16 +122,23 @@ class PropertyDescriptorResolver {
VariableElement field = members.getFields().get(name);
ExecutableElement getter = findMatchingGetter(members, getters, field);
TypeMirror propertyType = getter.getReturnType();
register(candidates, new JavaBeanPropertyDescriptor(getPropertyName(field, name), propertyType,
declaringElement, getter, members.getPublicSetter(name, propertyType), field, factoryMethod));
SourceMetadata sourceMetadata = this.environment.resolveSourceMetadata(field, getter);
register(candidates,
sourceMetadata.createPropertyDescriptor(getPropertyName(field, name),
(propertyName) -> new JavaBeanPropertyDescriptor(propertyName, propertyType,
declaringElement, getter, members.getPublicSetter(name, propertyType), field,
factoryMethod)));
});
// Then check for Lombok ones
members.getFields().forEach((name, field) -> {
TypeMirror propertyType = field.asType();
ExecutableElement getter = members.getPublicGetter(name, propertyType);
ExecutableElement setter = members.getPublicSetter(name, propertyType);
register(candidates, new LombokPropertyDescriptor(getPropertyName(field, name), propertyType,
declaringElement, getter, setter, field, factoryMethod));
SourceMetadata sourceMetadata = this.environment.resolveSourceMetadata(field, getter);
register(candidates,
sourceMetadata.createPropertyDescriptor(getPropertyName(field, name),
(propertyName) -> new LombokPropertyDescriptor(propertyName, propertyType, declaringElement,
getter, setter, field, factoryMethod)));
});
return candidates.values().stream();
}

View File

@@ -16,16 +16,14 @@
package org.springframework.boot.configurationprocessor;
import java.util.Arrays;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
/**
* A {@link PropertyDescriptor} for a record parameter.
*
@@ -44,8 +42,8 @@ class RecordParameterPropertyDescriptor extends ParameterPropertyDescriptor {
}
@Override
protected List<Element> getDeprecatableElements() {
return Arrays.asList(getGetter());
protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) {
return resolveItemDeprecation(environment, getGetter());
}
@Override

View File

@@ -72,6 +72,16 @@ public class ItemHint implements Comparable<ItemHint> {
return Collections.unmodifiableList(this.providers);
}
/**
* Return an {@link ItemHint} with the given prefix applied.
* @param prefix the prefix to apply
* @return a new {@link ItemHint} with the same of this instance whose property name
* has the prefix applied to it
*/
public ItemHint applyPrefix(String prefix) {
return new ItemHint(ConventionUtils.toDashedCase(prefix) + "." + this.name, this.values, this.providers);
}
@Override
public int compareTo(ItemHint other) {
return getName().compareTo(other.getName());

View File

@@ -18,13 +18,19 @@ package org.springframework.boot.configurationprocessor;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemIgnore;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.Metadata;
import org.springframework.boot.configurationprocessor.test.CompiledMetadataReader;
import org.springframework.boot.configurationprocessor.test.TestConfigurationMetadataAnnotationProcessor;
import org.springframework.boot.configurationsample.deprecation.Dbcp2Configuration;
import org.springframework.boot.configurationsample.method.NestedPropertiesMethod;
import org.springframework.boot.configurationsample.record.ExampleRecord;
@@ -47,6 +53,30 @@ import org.springframework.boot.configurationsample.simple.SimpleCollectionPrope
import org.springframework.boot.configurationsample.simple.SimplePrefixValueProperties;
import org.springframework.boot.configurationsample.simple.SimpleProperties;
import org.springframework.boot.configurationsample.simple.SimpleTypeProperties;
import org.springframework.boot.configurationsample.source.ConcreteProperties;
import org.springframework.boot.configurationsample.source.ConcreteSource;
import org.springframework.boot.configurationsample.source.ConcreteSourceAnnotated;
import org.springframework.boot.configurationsample.source.ConventionSource;
import org.springframework.boot.configurationsample.source.ConventionSourceAnnotated;
import org.springframework.boot.configurationsample.source.ImmutableSource;
import org.springframework.boot.configurationsample.source.ImmutableSourceAnnotated;
import org.springframework.boot.configurationsample.source.LombokSource;
import org.springframework.boot.configurationsample.source.LombokSourceAnnotated;
import org.springframework.boot.configurationsample.source.ParentWithHintProperties;
import org.springframework.boot.configurationsample.source.RecordSource;
import org.springframework.boot.configurationsample.source.RecordSourceAnnotated;
import org.springframework.boot.configurationsample.source.SimpleSource;
import org.springframework.boot.configurationsample.source.SimpleSourceAnnotated;
import org.springframework.boot.configurationsample.source.generation.AbstractPropertiesSource;
import org.springframework.boot.configurationsample.source.generation.ConfigurationPropertySourcesContainer;
import org.springframework.boot.configurationsample.source.generation.ConfigurationPropertySourcesContainer.First;
import org.springframework.boot.configurationsample.source.generation.ConfigurationPropertySourcesContainer.Second;
import org.springframework.boot.configurationsample.source.generation.ConfigurationPropertySourcesContainer.Third;
import org.springframework.boot.configurationsample.source.generation.ImmutablePropertiesSource;
import org.springframework.boot.configurationsample.source.generation.LombokPropertiesSource;
import org.springframework.boot.configurationsample.source.generation.NestedPropertiesSource;
import org.springframework.boot.configurationsample.source.generation.RecordPropertiesSources;
import org.springframework.boot.configurationsample.source.generation.SimplePropertiesSource;
import org.springframework.boot.configurationsample.specific.AnnotatedGetter;
import org.springframework.boot.configurationsample.specific.BoxingPojo;
import org.springframework.boot.configurationsample.specific.BuilderPojo;
@@ -69,6 +99,10 @@ import org.springframework.boot.configurationsample.specific.InvalidDoubleRegist
import org.springframework.boot.configurationsample.specific.SimplePojo;
import org.springframework.boot.configurationsample.specific.StaticAccessor;
import org.springframework.core.test.tools.CompilationException;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.ResourceFile;
import org.springframework.core.test.tools.SourceFile;
import org.springframework.core.test.tools.TestCompiler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -92,6 +126,7 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
assertThat(new ConfigurationMetadataAnnotationProcessor().getSupportedAnnotationTypes())
.containsExactlyInAnyOrder("org.springframework.boot.autoconfigure.AutoConfiguration",
"org.springframework.boot.context.properties.ConfigurationProperties",
"org.springframework.boot.context.properties.ConfigurationPropertiesSource",
"org.springframework.context.annotation.Configuration",
"org.springframework.boot.actuate.endpoint.annotation.Endpoint",
"org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint",
@@ -592,4 +627,388 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
assertThat(metadata.getIgnored()).containsExactly(ItemIgnore.forProperty("ignored.prop3"));
}
@Nested
class SourceTests {
@Test
void javaBeansSourceIsMergedWithNestedConfigurationProperty() {
ConfigurationMetadata metadata = compile(SimpleSourceAnnotated.class, SimpleSource.class);
assertThat(metadata).has(Metadata.withGroup("example.nested", SimpleSource.class))
.has(Metadata.withProperty("example.nested.name", String.class).withDescription("Name description."))
.has(Metadata.withProperty("example.nested.description", String.class)
.withDescription("Description description.")
.withDefaultValue("Hello World"))
.has(Metadata.withProperty("example.nested.type", String.class)
.withDescription("A property with a fixed set of values.")
.withDefaultValue("single"));
}
@Test
void lombokSourceIsMergedWithNestedConfigurationProperty() {
ConfigurationMetadata metadata = compile(LombokSourceAnnotated.class, LombokSource.class);
assertThat(metadata).has(Metadata.withGroup("example.nested", LombokSource.class))
.has(Metadata.withProperty("example.nested.name", String.class).withDescription("Name description."))
.has(Metadata.withProperty("example.nested.description", String.class)
.withDescription("Description description.")
.withDefaultValue("Hello World"))
.has(Metadata.withProperty("example.nested.type", String.class)
.withDescription("A property with a fixed set of values.")
.withDefaultValue("single"));
}
@Test
void immutableSourceIsMergedWithNestedConfigurationProperty() {
ConfigurationMetadata metadata = compile(ImmutableSourceAnnotated.class, ImmutableSource.class);
assertThat(metadata).has(Metadata.withGroup("example.nested", ImmutableSource.class))
.has(Metadata.withProperty("example.nested.name", String.class).withDescription("Name description."))
.has(Metadata.withProperty("example.nested.description", String.class)
.withDescription("Description description.")
.withDefaultValue("Hello World"))
.has(Metadata.withProperty("example.nested.type", String.class)
.withDescription("A property with a fixed set of values.")
.withDefaultValue("single"));
}
@Test
void recordSourceIsMergedWithNestedConfigurationProperty() {
ConfigurationMetadata metadata = compile(RecordSourceAnnotated.class, RecordSource.class);
assertThat(metadata).has(Metadata.withGroup("example.nested", RecordSource.class))
.has(Metadata.withProperty("example.nested.name", String.class).withDescription("Name description."))
.has(Metadata.withProperty("example.nested.description", String.class)
.withDescription("Description description.")
.withDefaultValue("Hello World"))
.has(Metadata.withProperty("example.nested.type", String.class)
.withDescription("A property with a fixed set of values.")
.withDefaultValue("single"));
}
@Test
void sourceIsMergedWithConfigurationProperties() {
ConfigurationMetadata metadata = compile(ParentWithHintProperties.class);
assertThat(metadata).has(Metadata.withGroup("example", ParentWithHintProperties.class))
.has(Metadata.withProperty("example.name", String.class).withDescription("Name description."))
.has(Metadata.withProperty("example.description", String.class)
.withDescription("Description description.")
.withDefaultValue("Hello World"))
.has(Metadata.withProperty("example.type", String.class)
.withDescription("A property with a fixed set of values.")
.withDefaultValue("single"))
.has(Metadata.withProperty("example.enabled", Boolean.class)
.withDescription("Whether this is enabled.")
.withDefaultValue(false));
}
@Test
void sourceHintIsMergedWithNestedConfigurationProperty() {
ConfigurationMetadata metadata = compile(SimpleSourceAnnotated.class);
assertThat(metadata).has(Metadata.withHint("example.nested.type")
.withValue(0, "auto", "Detect the type automatically.")
.withValue(1, "single", "Single type.")
.withValue(2, "multi", "Multi type."));
}
@Test
void sourceHintIsMergedWithConfigurationProperties() {
ConfigurationMetadata metadata = compile(ParentWithHintProperties.class);
assertThat(metadata).has(Metadata.withHint("example.type")
.withValue(0, "auto", "Detect the type automatically.")
.withValue(1, "single", "Single type.")
.withValue(2, "multi", "Multi type."));
}
@Test
void sourceWithNonCanonicalMetadataIsDiscovered() {
ConfigurationMetadata metadata = compile(ConventionSourceAnnotated.class);
assertThat(metadata).has(Metadata.withGroup("example.nested", ConventionSource.class))
.has(Metadata.withProperty("example.nested.first-name", String.class).withDescription("Camel case."))
.has(Metadata.withProperty("example.nested.last-name", String.class)
.withDescription("Canonical format."));
assertThat(metadata.getItems()).hasSize(4);
}
@Test
void sourceFromParentClasIsDiscoveredForConcreteSource() {
ConfigurationMetadata metadata = compile(ConcreteSourceAnnotated.class, ConcreteSource.class);
assertThat(metadata).has(Metadata.withGroup("example", ConcreteSourceAnnotated.class))
.has(Metadata.withGroup("example.nested", ConcreteSource.class))
.has(Metadata.withProperty("example.nested.enabled", Boolean.class)
.withDescription("Whether the feature is enabled."))
.has(Metadata.withProperty("example.nested.username", String.class)
.withDescription("User name.")
.withDefaultValue("user"))
.has(Metadata.withProperty("example.nested.password", String.class).withDescription("Password."));
assertThat(metadata.getItems()).hasSize(5);
}
@Test
void sourceFromParentClasIsDiscoveredForConfigurationProperties() {
ConfigurationMetadata metadata = compile(ConcreteProperties.class);
assertThat(metadata).has(Metadata.withGroup("example", ConcreteProperties.class))
.has(Metadata.withProperty("example.enabled", Boolean.class)
.withDescription("Whether the feature is enabled."))
.has(Metadata.withProperty("example.username", String.class)
.withDescription("User name.")
.withDefaultValue("user"))
.has(Metadata.withProperty("example.password", String.class).withDescription("Password."));
assertThat(metadata.getItems()).hasSize(4);
}
}
@Nested
class SourceGenerationTests {
@Test
void simplePropertiesSource() {
compile(withTestClasses(SimplePropertiesSource.class), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(SimplePropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."));
assertThat(metadata.getItems()).hasSize(2);
assertThat(metadata.getHints()).isEmpty();
});
}
@Test
void simplePropertiesSourceWithAdditionalMetadataIsMerged() {
String additionalMetadata = """
{
"properties": [
{
"name": "custom",
"type": "java.lang.Integer",
"description": "Custom property description."
}
]
}""";
compile(withTestClasses(SimplePropertiesSource.class)
.andThen(withAdditionalMetadata(SimplePropertiesSource.class, additionalMetadata)), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(SimplePropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."))
.has(Metadata.withProperty("custom")
.ofType(Integer.class)
.withDescription("Custom property description."));
assertThat(metadata.getItems()).hasSize(3);
});
}
@Test
void simplePropertiesSourceWithAdditionalMetadataHintIsMerged() {
String additionalMetadata = """
{
"hints": [
{
"name": "name",
"values": [
{ "value": "boot", "description": "Spring Boot." },
{ "value": "framework", "description": "Spring Framework." }
]
}
]
}""";
compile(withTestClasses(SimplePropertiesSource.class)
.andThen(withAdditionalMetadata(SimplePropertiesSource.class, additionalMetadata)), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(SimplePropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."))
.has(Metadata.withHint("name")
.withValue(0, "boot", "Spring Boot.")
.withValue(1, "framework", "Spring Framework."));
assertThat(metadata.getItems()).hasSize(2);
assertThat(metadata.getHints()).hasSize(1);
});
}
@Test
void simplePropertiesSourceWithAdditionalMetadataCanBeOverridden() {
String additionalMetadata = """
{
"properties": [
{
"name": "name",
"description": "Custom description."
}
]
}""";
compile(withTestClasses(SimplePropertiesSource.class)
.andThen(withAdditionalMetadata(SimplePropertiesSource.class, additionalMetadata)), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(SimplePropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Custom description."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."));
assertThat(metadata.getItems()).hasSize(2);
});
}
@Test
void lombokPropertiesSource() {
compile(withTestClasses(LombokPropertiesSource.class), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(LombokPropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."));
assertThat(metadata.getItems()).hasSize(2);
assertThat(metadata.getHints()).isEmpty();
});
}
@Test
void immutablePropertiesSource() {
compile(withTestClasses(ImmutablePropertiesSource.class), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(ImmutablePropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."));
assertThat(metadata.getItems()).hasSize(2);
assertThat(metadata.getHints()).isEmpty();
});
}
@Test
void recordPropertiesSource() {
compile(withTestClasses(RecordPropertiesSources.class), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(RecordPropertiesSources.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."));
assertThat(metadata.getItems()).hasSize(2);
assertThat(metadata.getHints()).isEmpty();
});
}
@Test
void abstractPropertiesSource() {
compile(withTestClasses(AbstractPropertiesSource.class), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(AbstractPropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name")
.ofType(String.class)
.withDefaultValue("boot")
.withDescription("Description of this simple property."))
.has(Metadata.withProperty("enabled")
.ofType(Boolean.class)
.withDefaultValue(false)
.withDescription("Whether it is enabled."));
assertThat(metadata.getItems()).hasSize(2);
assertThat(metadata.getHints()).isEmpty();
});
}
@Test
void nonRootConfigurationPropertiesSources() {
compile(withTestClasses(ConfigurationPropertySourcesContainer.class), (compiled) -> {
assertThat(CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(ConfigurationPropertySourcesContainer.class)))
.isNull();
ConfigurationMetadata firstMetadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(First.class));
assertThat(firstMetadata).isNotNull()
.has(Metadata.withProperty("name").ofType(String.class).withDescription("A name."));
assertThat(firstMetadata.getItems()).hasSize(1);
assertThat(firstMetadata.getHints()).isEmpty();
ConfigurationMetadata secondMetadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(Second.class));
assertThat(secondMetadata).isNotNull()
.has(Metadata.withProperty("visible")
.ofType(Boolean.class)
.withDefaultValue(true)
.withDescription("Whether this is visible."));
assertThat(secondMetadata.getItems()).hasSize(1);
assertThat(secondMetadata.getHints()).isEmpty();
assertThat(CompiledMetadataReader.getMetadata(compiled, getSourceMetadataLocation(Third.class)))
.isNull();
});
}
@Test
void nestedPropertiesSource() {
compile(withTestClasses(NestedPropertiesSource.class), (compiled) -> {
ConfigurationMetadata metadata = CompiledMetadataReader.getMetadata(compiled,
getSourceMetadataLocation(NestedPropertiesSource.class));
assertThat(metadata).isNotNull()
.has(Metadata.withProperty("name").ofType(String.class).withDescription("A name."))
.has(Metadata.withGroup("nested").ofType(NestedPropertiesSource.Nested.class))
.has(Metadata.withProperty("nested.name").ofType(String.class).withDescription("Another name."));
assertThat(metadata.getItems()).hasSize(3);
});
}
private String getSourceMetadataLocation(Class<?> type) {
return "META-INF/spring/configuration-metadata/%s.json".formatted(type.getName());
}
private void compile(Function<TestCompiler, TestCompiler> configuration, Consumer<Compiled> compiled) {
TestCompiler testCompiler = TestCompiler.forSystem();
configuration.apply(testCompiler)
.withProcessors(new TestConfigurationMetadataAnnotationProcessor())
.compile(compiled);
}
private Function<TestCompiler, TestCompiler> withTestClasses(Class<?>... testClasses) {
return (compiler) -> compiler
.withSources(Arrays.stream(testClasses).map(SourceFile::forTestClass).toList());
}
private Function<TestCompiler, TestCompiler> withAdditionalMetadata(Class<?> type, String content) {
String location = "META-INF/spring/configuration-metadata/additional/%s.json".formatted(type.getName());
return (compiler) -> compiler.withResources(ResourceFile.of(location, content));
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2012-2025 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.configurationprocessor;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemHint.ValueHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
import org.springframework.boot.configurationprocessor.metadata.Metadata;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MetadataCollector}.
*
* @author Stephane Nicoll
*/
class MetadataCollectorTests {
private static final Predicate<ItemMetadata> NO_MERGE = (metadata) -> false;
private static final ConfigurationMetadata SINGLE_ITEM_METADATA = readMetadata("""
{
"properties": [
{ "name": "name", "type": "java.lang.String" }
]
}
""");
@Test
void addSingleItemMetadata() {
MetadataCollector collector = createSimpleCollector();
collector.add(SINGLE_ITEM_METADATA.getItems().get(0));
assertThat(collector.getMetadata()).has(Metadata.withProperty("name", String.class));
}
@Test
void addIfAbsentAddsPropertyIfItDoesNotExist() {
MetadataCollector collector = createSimpleCollector();
collector.addIfAbsent(SINGLE_ITEM_METADATA.getItems().get(0));
ConfigurationMetadata metadata = collector.getMetadata();
assertThat(metadata).has(Metadata.withProperty("name", String.class));
assertThat(metadata.getItems()).hasSize(1);
}
@Test
void addIfAbsentIgnoresExistingProperty() {
MetadataCollector collector = createSimpleCollector();
collector.addIfAbsent(SINGLE_ITEM_METADATA.getItems().get(0));
collector.addIfAbsent(SINGLE_ITEM_METADATA.getItems().get(0));
collector.addIfAbsent(SINGLE_ITEM_METADATA.getItems().get(0));
ConfigurationMetadata metadata = collector.getMetadata();
assertThat(metadata).has(Metadata.withProperty("name", String.class));
assertThat(metadata.getItems()).hasSize(1);
}
@Test
@SuppressWarnings("unchecked")
void addNewMetadataDoesNotInvokeConflictResolution() {
MetadataCollector collector = createSimpleCollector();
Consumer<ItemMetadata> conflictResolution = mock(Consumer.class);
collector.add(SINGLE_ITEM_METADATA.getItems().get(0), conflictResolution);
then(conflictResolution).shouldHaveNoInteractions();
}
@SuppressWarnings("unchecked")
@Test
void addMetadataWithExistingInstanceInvokesConflictResolution() {
MetadataCollector collector = createSimpleCollector();
ItemMetadata metadata = SINGLE_ITEM_METADATA.getItems().get(0);
collector.add(metadata);
Consumer<ItemMetadata> conflictResolution = mock(Consumer.class);
collector.add(metadata, conflictResolution);
then(conflictResolution).should().accept(metadata);
}
@Test
void addSingleItemHint() {
MetadataCollector collector = createSimpleCollector();
collector.add(SINGLE_ITEM_METADATA.getItems().get(0));
ValueHint firstValueHint = new ValueHint("one", "First.");
ValueHint secondValueHint = new ValueHint("two", "Second.");
ItemHint itemHint = new ItemHint("name", List.of(firstValueHint, secondValueHint), Collections.emptyList());
collector.add(itemHint);
assertThat(collector.getMetadata())
.has(Metadata.withHint("name").withValue(0, "one", "First.").withValue(1, "two", "Second."));
}
@Test
@SuppressWarnings("unchecked")
void getMetadataDoesNotInvokeMergeFunctionIfPreviousMetadataIsNull() {
Predicate<ItemMetadata> mergedRequired = mock(Predicate.class);
MetadataCollector collector = new MetadataCollector(mergedRequired, null);
collector.add(SINGLE_ITEM_METADATA.getItems().get(0));
collector.getMetadata();
then(mergedRequired).shouldHaveNoInteractions();
}
@Test
@SuppressWarnings("unchecked")
void getMetadataAddPreviousItemIfMergeFunctionReturnsTrue() {
Predicate<ItemMetadata> mergedRequired = mock(Predicate.class);
ItemMetadata itemMetadata = SINGLE_ITEM_METADATA.getItems().get(0);
given(mergedRequired.test(itemMetadata)).willReturn(true);
MetadataCollector collector = new MetadataCollector(mergedRequired, SINGLE_ITEM_METADATA);
assertThat(collector.getMetadata()).has(Metadata.withProperty("name", String.class));
then(mergedRequired).should().test(itemMetadata);
}
@Test
@SuppressWarnings("unchecked")
void getMetadataDoesNotAddPreviousItemIfMergeFunctionReturnsFalse() {
Predicate<ItemMetadata> mergedRequired = mock(Predicate.class);
ItemMetadata itemMetadata = SINGLE_ITEM_METADATA.getItems().get(0);
given(mergedRequired.test(itemMetadata)).willReturn(false);
MetadataCollector collector = new MetadataCollector(mergedRequired, SINGLE_ITEM_METADATA);
assertThat(collector.getMetadata().getItems()).isEmpty();
then(mergedRequired).should().test(itemMetadata);
}
private MetadataCollector createSimpleCollector() {
return new MetadataCollector(NO_MERGE, null);
}
private static ConfigurationMetadata readMetadata(String json) {
try {
JsonMarshaller marshaller = new JsonMarshaller();
return marshaller.read(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
}
catch (Exception ex) {
throw new IllegalStateException("Invalid JSON: " + json, ex);
}
}
}

View File

@@ -42,6 +42,7 @@ class MetadataGenerationEnvironmentFactory implements Function<ProcessingEnviron
TestConfigurationMetadataAnnotationProcessor.WEB_ENDPOINT_ANNOTATION));
return new MetadataGenerationEnvironment(environment,
TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.NESTED_CONFIGURATION_PROPERTY_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.CONSTRUCTOR_BINDING_ANNOTATION,

View File

@@ -41,7 +41,7 @@ class MetadataStoreTests {
private final ProcessingEnvironment environment = mock(ProcessingEnvironment.class);
private final MetadataStore metadataStore = new MetadataStore(this.environment);
private final MetadataStore metadataStore = new MetadataStore(this.environment, mock(TypeUtils.class));
@Test
void additionalMetadataIsLocatedInMavenBuild() throws IOException {
@@ -52,7 +52,8 @@ class MetadataStoreTests {
File additionalMetadata = new File(metaInf, "additional-spring-configuration-metadata.json");
additionalMetadata.createNewFile();
assertThat(this.metadataStore.locateAdditionalMetadataFile(
new File(classesLocation, "META-INF/additional-spring-configuration-metadata.json")))
new File(classesLocation, "META-INF/additional-spring-configuration-metadata.json"),
"META-INF/additional-spring-configuration-metadata.json"))
.isEqualTo(additionalMetadata);
}
@@ -66,7 +67,8 @@ class MetadataStoreTests {
File additionalMetadata = new File(metaInf, "additional-spring-configuration-metadata.json");
additionalMetadata.createNewFile();
assertThat(this.metadataStore.locateAdditionalMetadataFile(
new File(classesLocation, "META-INF/additional-spring-configuration-metadata.json")))
new File(classesLocation, "META-INF/additional-spring-configuration-metadata.json"),
"META-INF/additional-spring-configuration-metadata.json"))
.isEqualTo(additionalMetadata);
}
@@ -80,7 +82,8 @@ class MetadataStoreTests {
File additionalMetadata = new File(metaInf, "additional-spring-configuration-metadata.json");
additionalMetadata.createNewFile();
assertThat(this.metadataStore.locateAdditionalMetadataFile(
new File(classesLocation, "META-INF/additional-spring-configuration-metadata.json")))
new File(classesLocation, "META-INF/additional-spring-configuration-metadata.json"),
"META-INF/additional-spring-configuration-metadata.json"))
.isEqualTo(additionalMetadata);
}
@@ -95,7 +98,9 @@ class MetadataStoreTests {
given(this.environment.getOptions()).willReturn(
Collections.singletonMap(ConfigurationMetadataAnnotationProcessor.ADDITIONAL_METADATA_LOCATIONS_OPTION,
location.getAbsolutePath()));
assertThat(this.metadataStore.locateAdditionalMetadataFile(new File(app, "foo"))).isEqualTo(additionalMetadata);
assertThat(this.metadataStore.locateAdditionalMetadataFile(new File(app, "foo"),
"META-INF/additional-spring-configuration-metadata.json"))
.isEqualTo(additionalMetadata);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 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.configurationprocessor.metadata;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.configurationprocessor.metadata.ItemHint.ValueHint;
import org.springframework.boot.configurationprocessor.metadata.ItemHint.ValueProvider;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ItemHint}.
*
* @author Stephane Nicoll
*/
class ItemHintTests {
@Test
void prefixIsAppliedWithValueHint() {
ValueHint firstValueHint = new ValueHint("one", "First.");
ValueHint secondValueHint = new ValueHint("two", "Second.");
ItemHint itemHint = new ItemHint("name", List.of(firstValueHint, secondValueHint), Collections.emptyList());
ItemHint prefixedItemHint = itemHint.applyPrefix("example");
assertThat(itemHint).isNotSameAs(prefixedItemHint);
assertThat(prefixedItemHint.getName()).isEqualTo("example.name");
assertThat(prefixedItemHint.getValues()).containsExactly(firstValueHint, secondValueHint);
assertThat(prefixedItemHint.getProviders()).isEmpty();
}
@Test
void prefixIsAppliedWithValueProvider() {
ValueProvider firstValueProvider = new ValueProvider("class-reference", Map.of("target", String.class));
ValueProvider secondValueProvider = new ValueProvider("any", Collections.emptyMap());
ItemHint itemHint = new ItemHint("name", Collections.emptyList(),
List.of(firstValueProvider, secondValueProvider));
ItemHint prefixedItemHint = itemHint.applyPrefix("example");
assertThat(itemHint).isNotSameAs(prefixedItemHint);
assertThat(prefixedItemHint.getName()).isEqualTo("example.name");
assertThat(prefixedItemHint.getValues()).isEmpty();
assertThat(prefixedItemHint.getProviders()).containsExactly(firstValueProvider, secondValueProvider);
}
@Test
void prefixIsAppliedWithConvention() {
ItemHint itemHint = new ItemHint("name", Collections.emptyList(), Collections.emptyList());
ItemHint prefixedItemHint = itemHint.applyPrefix("example.nestedType");
assertThat(prefixedItemHint.getName()).isEqualTo("example.nested-type.name");
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.core.test.tools.TestCompiler;
* Read the contents of metadata generated from the {@link TestCompiler}.
*
* @author Scott Frederick
* @author Stephane Nicoll
*/
public final class CompiledMetadataReader {
@@ -36,7 +37,11 @@ public final class CompiledMetadataReader {
}
public static ConfigurationMetadata getMetadata(Compiled compiled) {
InputStream inputStream = compiled.getClassLoader().getResourceAsStream(METADATA_FILE);
return getMetadata(compiled, METADATA_FILE);
}
public static ConfigurationMetadata getMetadata(Compiled compiled, String location) {
InputStream inputStream = compiled.getClassLoader().getResourceAsStream(location);
try {
if (inputStream != null) {
return new JsonMarshaller().read(inputStream);
@@ -46,7 +51,7 @@ public final class CompiledMetadataReader {
}
}
catch (Exception ex) {
throw new RuntimeException("Failed to read metadata", ex);
throw new RuntimeException("Failed to read metadata fom '%s'".formatted(location), ex);
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.boot.configurationprocessor.ConfigurationMetadataAnno
* @author Scott Frederick
*/
@SupportedAnnotationTypes({ TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.CONTROLLER_ENDPOINT_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.ENDPOINT_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.JMX_ENDPOINT_ANNOTATION,
@@ -48,6 +49,8 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
public static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.configurationsample.ConfigurationProperties";
public static final String CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION = "org.springframework.boot.configurationsample.ConfigurationPropertiesSource";
public static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.NestedConfigurationProperty";
public static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.DeprecatedConfigurationProperty";
@@ -86,6 +89,11 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
return CONFIGURATION_PROPERTIES_ANNOTATION;
}
@Override
protected String configurationPropertiesSourceAnnotation() {
return CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION;
}
@Override
protected String nestedConfigurationPropertyAnnotation() {
return NESTED_CONFIGURATION_PROPERTY_ANNOTATION;

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-2025 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.configurationsample;
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;
/**
* Alternative to Spring Boot's {@code @ConfigurationProperties} for testing (removes the
* need for a dependency on the real annotation).
*
* @author Stephane Nicoll
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertiesSource {
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
/**
* Test properties contributed by a parent source.
*/
public abstract class BaseSource {
private boolean enabled;
private String username;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
protected abstract String getPassword();
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
@ConfigurationProperties("example")
public class ConcreteProperties extends BaseSource {
/**
* Password.
*/
private String password;
@Override
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
public class ConcreteSource extends BaseSource {
/**
* Password.
*/
private String password;
@Override
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
@ConfigurationProperties("example")
public class ConcreteSourceAnnotated {
@NestedConfigurationProperty
private final ConcreteSource nested = new ConcreteSource();
public ConcreteSource getNested() {
return this.nested;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
/**
* Test various convention-based lookups.
*/
public class ConventionSource {
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
@ConfigurationProperties(prefix = "example")
public class ConventionSourceAnnotated {
@NestedConfigurationProperty
private final ConventionSource nested = new ConventionSource();
public ConventionSource getNested() {
return this.nested;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
/**
* Immutable type with manual metadata. This illustrates the case where the type of a
* property is defined in a separate class and source-based metadata cannot be discovered.
*/
public class ImmutableSource {
private final String name;
private final String description;
private final String type;
public ImmutableSource(String name, String description, String type) {
this.name = name;
this.description = description;
this.type = type;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
@ConfigurationProperties("example")
public class ImmutableSourceAnnotated {
@NestedConfigurationProperty
private final ImmutableSource nested;
public ImmutableSourceAnnotated(ImmutableSource nested) {
this.nested = nested;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import lombok.Getter;
import lombok.Setter;
/**
* Type using lombok with manual metadata. This illustrates the case where the type of a
* property is defined in a separate class and source-based metadata cannot be discovered.
*/
@Getter
@Setter
public class LombokSource {
private String name;
private String description;
private String type;
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import lombok.Getter;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
@Getter
@ConfigurationProperties("example")
public class LombokSourceAnnotated {
@NestedConfigurationProperty
private final LombokSource nested = new LombokSource();
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
@ConfigurationProperties("example")
public class ParentWithHintProperties extends SimpleSource {
/**
* Whether this is enabled.
*/
private boolean enabled;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
/**
* Record type with manual metadata. This illustrates the case where the type of a
* property is defined in a separate class and source-based metadata cannot be discovered.
*/
public record RecordSource(String name, String description, String type) {
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
@ConfigurationProperties("example")
public record RecordSourceAnnotated(@NestedConfigurationProperty RecordSource nested) {
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
/**
* Type with manual metadata. This illustrates the case where the type of a property is
* defined in a separate class and source-based metadata cannot be discovered.
*/
public class SimpleSource {
private String name;
private String description;
private String type;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public String getType() {
return this.type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2025 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.configurationsample.source;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
@ConfigurationProperties("example")
public class SimpleSourceAnnotated {
@NestedConfigurationProperty
private final SimpleSource nested = new SimpleSource();
public SimpleSource getNested() {
return this.nested;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
@ConfigurationPropertiesSource
public abstract class AbstractPropertiesSource {
/**
* Description of this simple property.
*/
private String name = "boot";
/**
* Whether it is enabled.
*/
private boolean enabled;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
public class ConfigurationPropertySourcesContainer {
@ConfigurationPropertiesSource
public static class First {
/**
* A name.
*/
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
@ConfigurationPropertiesSource
public static class Second {
/**
* Whether this is visible.
*/
private boolean visible = true;
public boolean isVisible() {
return this.visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
}
public static class Third {
private String shouldBeIgnored;
public String getShouldBeIgnored() {
return this.shouldBeIgnored;
}
public void setShouldBeIgnored(String shouldBeIgnored) {
this.shouldBeIgnored = shouldBeIgnored;
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
import org.springframework.boot.configurationsample.DefaultValue;
@ConfigurationPropertiesSource
public class ImmutablePropertiesSource {
/**
* Description of this simple property.
*/
private final String name;
/**
* Whether it is enabled.
*/
private final boolean enabled;
public ImmutablePropertiesSource(@DefaultValue("boot") String name, boolean enabled) {
this.name = name;
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
@Getter
@Setter
@ConfigurationPropertiesSource
public class LombokPropertiesSource {
/**
* Description of this simple property.
*/
private String name = "boot";
/**
* Whether it is enabled.
*/
private boolean enabled;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
@ConfigurationPropertiesSource
public class NestedPropertiesSource {
/**
* A name.
*/
private String name;
private final Nested nested = new Nested();
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Nested getNested() {
return this.nested;
}
public static class Nested {
/**
* Another name.
*/
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
import org.springframework.boot.configurationsample.DefaultValue;
/**
* Sample record properties source.
*
* @param name Description of this simple property.
* @param enabled Whether it is enabled.
* @author Stephane Nicoll
*/
@ConfigurationPropertiesSource
public record RecordPropertiesSources(@DefaultValue("boot") String name, boolean enabled) {
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2025 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.configurationsample.source.generation;
import org.springframework.boot.configurationsample.ConfigurationPropertiesSource;
@ConfigurationPropertiesSource
public class SimplePropertiesSource {
/**
* Description of this simple property.
*/
private String name = "boot";
/**
* Whether it is enabled.
*/
private boolean enabled;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,15 @@
{
"properties": [
{
"name": "enabled",
"type": "java.lang.Boolean",
"description": "Whether the feature is enabled."
},
{
"name": "username",
"type": "java.lang.String",
"description": "User name.",
"defaultValue": "user"
}
]
}

View File

@@ -0,0 +1,14 @@
{
"properties": [
{
"name": "firstName",
"type": "java.lang.String",
"description": "Camel case."
},
{
"name": "last-name",
"type": "java.lang.String",
"description": "Canonical format."
}
]
}

View File

@@ -0,0 +1,40 @@
{
"properties": [
{
"name": "name",
"type": "java.lang.String",
"description": "Name description."
},
{
"name": "description",
"type": "java.lang.String",
"description": "Description description.",
"defaultValue": "Hello World"
},
{
"name": "type",
"type": "java.lang.String",
"description": "A property with a fixed set of values.",
"defaultValue": "single"
}
],
"hints": [
{
"name": "type",
"values": [
{
"value": "auto",
"description": "Detect the type automatically."
},
{
"value": "single",
"description": "Single type."
},
{
"value": "multi",
"description": "Multi type."
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
{
"properties": [
{
"name": "name",
"type": "java.lang.String",
"description": "Name description."
},
{
"name": "description",
"type": "java.lang.String",
"description": "Description description.",
"defaultValue": "Hello World"
},
{
"name": "type",
"type": "java.lang.String",
"description": "A property with a fixed set of values.",
"defaultValue": "single"
}
],
"hints": [
{
"name": "type",
"values": [
{
"value": "auto",
"description": "Detect the type automatically."
},
{
"value": "single",
"description": "Single type."
},
{
"value": "multi",
"description": "Multi type."
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
{
"properties": [
{
"name": "name",
"type": "java.lang.String",
"description": "Name description."
},
{
"name": "description",
"type": "java.lang.String",
"description": "Description description.",
"defaultValue": "Hello World"
},
{
"name": "type",
"type": "java.lang.String",
"description": "A property with a fixed set of values.",
"defaultValue": "single"
}
],
"hints": [
{
"name": "type",
"values": [
{
"value": "auto",
"description": "Detect the type automatically."
},
{
"value": "single",
"description": "Single type."
},
{
"value": "multi",
"description": "Multi type."
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
{
"properties": [
{
"name": "name",
"type": "java.lang.String",
"description": "Name description."
},
{
"name": "description",
"type": "java.lang.String",
"description": "Description description.",
"defaultValue": "Hello World"
},
{
"name": "type",
"type": "java.lang.String",
"description": "A property with a fixed set of values.",
"defaultValue": "single"
}
],
"hints": [
{
"name": "type",
"values": [
{
"value": "auto",
"description": "Detect the type automatically."
},
{
"value": "single",
"description": "Single type."
},
{
"value": "multi",
"description": "Multi type."
}
]
}
]
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2025 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;
/**
* Indicates that the annotated type is a source of configuration properties metadata.
* <p>
* This annotation has no effect on the actual binding process, but serves as a hint to
* the {@code spring-boot-configuration-processor} to generate full metadata for the type.
* <p>
* Typically, this annotation is only required for types located in a different module
* than the {@code @ConfigurationProperties} class that references them. When both types
* are in the same module, the annotation processor can automatically discover full
* metadata as long as the source is available.
* <p>
* Use this annotation when metadata for types located outside the module is needed:
* <ol>
* <li>Nested types annotated by {@code @NestedConfigurationProperty}</li>
* <li>Base classes that a {@code @ConfigurationProperties}-annotated type extends
* from</li>
* </ol>
* <p>
* In the example below, {@code ServerProperties} is located in module "A" and
* {@code Host} in module "B":<pre><code class="java">
* &#064;ConfigurationProperties("example.server")
* class ServerProperties {
*
* &#064;NestedConfigurationProperty
* private final Host host = new Host();
*
* public Host getHost() { ... }
*
* // Other properties, getter, setter.
*
* }</code></pre>
* <p>
* Properties from {@code Host} are detected as they are based on the type, but
* description and default value are not. To fix this, add the
* {@code spring-boot-configuration-processor} to module "B" if it is not present already
* and update {@code Host} as follows::<pre><code class="java">
* &#064;ConfigurationPropertiesSource
* class Host {
*
* /**
* * URL to use.
* *&#47;
* private String url = "https://example.com";
*
* // Other properties, getter, setter.
*
* }</code></pre>
* <p>
* Similarly the metadata of a base class that a
* {@code @ConfigurationProperties}-annotated type extends from can also be detected.
* Consider the following example:<pre><code class="java">
* &#064;ConfigurationProperties("example.client.github")
* class GitHubClientProperties extends AbstractClientProperties {
*
* // Additional properties, getter, setter.
*
* }</code></pre>
* <p>
* As with nested types, adding {@code @ConfigurationPropertiesSource} to
* {@code AbstractClientProperties} and the {@code spring-boot-configuration-processor} to
* its module ensures full metadata generation.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertiesSource {
}