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

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