Use @ConstructorBinding when generating meta-data
Update the configuration processor to use the newly introduced `@ConstructorBinding` annotation to determine when meta data should be generated from constructor parameters. Prior to this commit, the processor had no good way to tell when constructor parameters should be used instead of getters/setters. Closes gh-17035
This commit is contained in:
committed by
Phillip Webb
parent
4208c93ba7
commit
45f6668d03
@@ -22,7 +22,6 @@ import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -39,7 +38,6 @@ 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.DeclaredType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.util.ElementFilter;
|
||||
import javax.tools.Diagnostic.Kind;
|
||||
@@ -73,6 +71,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot."
|
||||
+ "context.properties.DeprecatedConfigurationProperty";
|
||||
|
||||
static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.context.properties.ConstructorBinding";
|
||||
|
||||
static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.context.properties.bind.DefaultValue";
|
||||
|
||||
static final String ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.Endpoint";
|
||||
@@ -101,6 +101,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
return DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION;
|
||||
}
|
||||
|
||||
protected String constructorBindingAnnotation() {
|
||||
return CONSTRUCTOR_BINDING_ANNOTATION;
|
||||
}
|
||||
|
||||
protected String defaultValueAnnotation() {
|
||||
return DEFAULT_VALUE_ANNOTATION;
|
||||
}
|
||||
@@ -130,7 +134,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata());
|
||||
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
|
||||
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
|
||||
defaultValueAnnotation(), endpointAnnotation(), readOperationAnnotation());
|
||||
constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotation(),
|
||||
readOperationAnnotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,38 +164,16 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
|
||||
private Map<Element, List<Element>> getElementsAnnotatedOrMetaAnnotatedWith(RoundEnvironment roundEnv,
|
||||
TypeElement annotation) {
|
||||
DeclaredType annotationType = (DeclaredType) annotation.asType();
|
||||
Map<Element, List<Element>> result = new LinkedHashMap<>();
|
||||
for (Element element : roundEnv.getRootElements()) {
|
||||
LinkedList<Element> stack = new LinkedList<>();
|
||||
stack.push(element);
|
||||
collectElementsAnnotatedOrMetaAnnotatedWith(annotationType, stack);
|
||||
stack.removeFirst();
|
||||
if (!stack.isEmpty()) {
|
||||
result.put(element, Collections.unmodifiableList(stack));
|
||||
List<Element> annotations = this.metadataEnv.getElementsAnnotatedOrMetaAnnotatedWith(element, annotation);
|
||||
if (!annotations.isEmpty()) {
|
||||
result.put(element, annotations);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean collectElementsAnnotatedOrMetaAnnotatedWith(DeclaredType annotationType,
|
||||
LinkedList<Element> stack) {
|
||||
Element element = stack.peekLast();
|
||||
for (AnnotationMirror annotation : this.processingEnv.getElementUtils().getAllAnnotationMirrors(element)) {
|
||||
Element annotationElement = annotation.getAnnotationType().asElement();
|
||||
if (!stack.contains(annotationElement)) {
|
||||
stack.addLast(annotationElement);
|
||||
if (annotationElement.equals(annotationType.asElement())) {
|
||||
return true;
|
||||
}
|
||||
if (!collectElementsAnnotatedOrMetaAnnotatedWith(annotationType, stack)) {
|
||||
stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void processElement(Element element) {
|
||||
try {
|
||||
AnnotationMirror annotation = this.metadataEnv.getConfigurationPropertiesAnnotation(element);
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -86,6 +87,8 @@ class MetadataGenerationEnvironment {
|
||||
|
||||
private final String deprecatedConfigurationPropertyAnnotation;
|
||||
|
||||
private final String constructorBindingAnnotation;
|
||||
|
||||
private final String defaultValueAnnotation;
|
||||
|
||||
private final String endpointAnnotation;
|
||||
@@ -94,7 +97,8 @@ class MetadataGenerationEnvironment {
|
||||
|
||||
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
|
||||
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
|
||||
String defaultValueAnnotation, String endpointAnnotation, String readOperationAnnotation) {
|
||||
String constructorBindingAnnotation, String defaultValueAnnotation, String endpointAnnotation,
|
||||
String readOperationAnnotation) {
|
||||
this.typeUtils = new TypeUtils(environment);
|
||||
this.elements = environment.getElementUtils();
|
||||
this.messager = environment.getMessager();
|
||||
@@ -102,6 +106,7 @@ class MetadataGenerationEnvironment {
|
||||
this.configurationPropertiesAnnotation = configurationPropertiesAnnotation;
|
||||
this.nestedConfigurationPropertyAnnotation = nestedConfigurationPropertyAnnotation;
|
||||
this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation;
|
||||
this.constructorBindingAnnotation = constructorBindingAnnotation;
|
||||
this.defaultValueAnnotation = defaultValueAnnotation;
|
||||
this.endpointAnnotation = endpointAnnotation;
|
||||
this.readOperationAnnotation = readOperationAnnotation;
|
||||
@@ -170,6 +175,14 @@ class MetadataGenerationEnvironment {
|
||||
return new ItemDeprecation(reason, replacement);
|
||||
}
|
||||
|
||||
boolean hasConstructorBindingAnnotation(TypeElement typeElement) {
|
||||
return hasAnnotationRecursive(typeElement, this.constructorBindingAnnotation);
|
||||
}
|
||||
|
||||
boolean hasConstructorBindingAnnotation(ExecutableElement element) {
|
||||
return hasAnnotation(element, this.constructorBindingAnnotation);
|
||||
}
|
||||
|
||||
boolean hasAnnotation(Element element, String type) {
|
||||
return getAnnotation(element, type) != null;
|
||||
}
|
||||
@@ -185,6 +198,42 @@ class MetadataGenerationEnvironment {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the annotations that are annotated or meta-annotated with the specified
|
||||
* {@link TypeElement annotation}.
|
||||
* @param element the element to inspect
|
||||
* @param annotationType the annotation to discover
|
||||
* @return the annotations that are annotated or meta-annotated with this annotation
|
||||
*/
|
||||
List<Element> getElementsAnnotatedOrMetaAnnotatedWith(Element element, TypeElement annotationType) {
|
||||
LinkedList<Element> stack = new LinkedList<>();
|
||||
stack.push(element);
|
||||
collectElementsAnnotatedOrMetaAnnotatedWith(annotationType, stack);
|
||||
stack.removeFirst();
|
||||
return Collections.unmodifiableList(stack);
|
||||
}
|
||||
|
||||
private boolean hasAnnotationRecursive(Element element, String type) {
|
||||
return !getElementsAnnotatedOrMetaAnnotatedWith(element, this.elements.getTypeElement(type)).isEmpty();
|
||||
}
|
||||
|
||||
private boolean collectElementsAnnotatedOrMetaAnnotatedWith(TypeElement annotationType, LinkedList<Element> stack) {
|
||||
Element element = stack.peekLast();
|
||||
for (AnnotationMirror annotation : this.elements.getAllAnnotationMirrors(element)) {
|
||||
Element annotationElement = annotation.getAnnotationType().asElement();
|
||||
if (!stack.contains(annotationElement)) {
|
||||
stack.addLast(annotationElement);
|
||||
if (annotationElement.equals(annotationType)) {
|
||||
return true;
|
||||
}
|
||||
if (!collectElementsAnnotatedOrMetaAnnotatedWith(annotationType, stack)) {
|
||||
stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, Object> getAnnotationElementValues(AnnotationMirror annotation) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
annotation.getElementValues()
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.configurationprocessor;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
@@ -31,6 +32,7 @@ import javax.lang.model.util.ElementFilter;
|
||||
* Resolve {@link PropertyDescriptor} instances.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertyDescriptorResolver {
|
||||
|
||||
@@ -54,13 +56,19 @@ class PropertyDescriptorResolver {
|
||||
if (factoryMethod != null) {
|
||||
return resolveJavaBeanProperties(type, factoryMethod, members);
|
||||
}
|
||||
ExecutableElement constructor = resolveConstructor(type);
|
||||
if (constructor != null) {
|
||||
return resolveConstructorProperties(type, factoryMethod, members, constructor);
|
||||
}
|
||||
else {
|
||||
return resolveJavaBeanProperties(type, factoryMethod, members);
|
||||
return resolve(ConfigurationPropertiesTypeElement.of(type, this.environment), factoryMethod, members);
|
||||
}
|
||||
|
||||
private Stream<PropertyDescriptor<?>> resolve(ConfigurationPropertiesTypeElement type,
|
||||
ExecutableElement factoryMethod, TypeElementMembers members) {
|
||||
if (type.isConstructorBindingEnabled()) {
|
||||
ExecutableElement constructor = type.getBindConstructor();
|
||||
if (constructor != null) {
|
||||
return resolveConstructorProperties(type.getType(), factoryMethod, members, constructor);
|
||||
}
|
||||
return Stream.empty();
|
||||
}
|
||||
return resolveJavaBeanProperties(type.getType(), factoryMethod, members);
|
||||
}
|
||||
|
||||
Stream<PropertyDescriptor<?>> resolveConstructorProperties(TypeElement type, ExecutableElement factoryMethod,
|
||||
@@ -108,12 +116,66 @@ class PropertyDescriptorResolver {
|
||||
return descriptor.isProperty(this.environment) || descriptor.isNested(this.environment);
|
||||
}
|
||||
|
||||
private ExecutableElement resolveConstructor(TypeElement type) {
|
||||
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
|
||||
if (constructors.size() == 1 && constructors.get(0).getParameters().size() > 0) {
|
||||
return constructors.get(0);
|
||||
/**
|
||||
* Wrapper around a {@link TypeElement} that could be bound.
|
||||
*/
|
||||
private static class ConfigurationPropertiesTypeElement {
|
||||
|
||||
private final TypeElement type;
|
||||
|
||||
private final boolean constructorBoundType;
|
||||
|
||||
private final List<ExecutableElement> constructors;
|
||||
|
||||
private final List<ExecutableElement> boundConstructors;
|
||||
|
||||
ConfigurationPropertiesTypeElement(TypeElement type, boolean constructorBoundType,
|
||||
List<ExecutableElement> constructors, List<ExecutableElement> boundConstructors) {
|
||||
this.type = type;
|
||||
this.constructorBoundType = constructorBoundType;
|
||||
this.constructors = constructors;
|
||||
this.boundConstructors = boundConstructors;
|
||||
}
|
||||
return null;
|
||||
|
||||
TypeElement getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
boolean isConstructorBindingEnabled() {
|
||||
return this.constructorBoundType || !this.boundConstructors.isEmpty();
|
||||
}
|
||||
|
||||
ExecutableElement getBindConstructor() {
|
||||
if (this.constructorBoundType && this.boundConstructors.isEmpty()) {
|
||||
return findBoundConstructor();
|
||||
}
|
||||
if (this.boundConstructors.size() == 1) {
|
||||
return this.boundConstructors.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ExecutableElement findBoundConstructor() {
|
||||
ExecutableElement boundConstructor = null;
|
||||
for (ExecutableElement canidate : this.constructors) {
|
||||
if (!canidate.getParameters().isEmpty()) {
|
||||
if (boundConstructor != null) {
|
||||
return null;
|
||||
}
|
||||
boundConstructor = canidate;
|
||||
}
|
||||
}
|
||||
return boundConstructor;
|
||||
}
|
||||
|
||||
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
|
||||
boolean constructorBoundType = env.hasConstructorBindingAnnotation(type);
|
||||
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
|
||||
List<ExecutableElement> boundConstructors = constructors.stream()
|
||||
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
|
||||
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user