diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index 2bc303d7ab8..172bbfb9f43 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -59,23 +59,22 @@ class PropertyDescriptorResolver { if (factoryMethod != null) { return resolveJavaBeanProperties(type, factoryMethod, members); } - return resolve(ConfigurationPropertiesTypeElement.of(type, this.environment), factoryMethod, members); + return resolve(ConfigurationPropertiesTypeElement.of(type, this.environment), members); } - private Stream> resolve(ConfigurationPropertiesTypeElement type, - ExecutableElement factoryMethod, TypeElementMembers members) { + private Stream> resolve(ConfigurationPropertiesTypeElement type, TypeElementMembers members) { if (type.isConstructorBindingEnabled()) { ExecutableElement constructor = type.getBindConstructor(); if (constructor != null) { - return resolveConstructorProperties(type.getType(), factoryMethod, members, constructor); + return resolveConstructorProperties(type.getType(), members, constructor); } return Stream.empty(); } - return resolveJavaBeanProperties(type.getType(), factoryMethod, members); + return resolveJavaBeanProperties(type.getType(), null, members); } - Stream> resolveConstructorProperties(TypeElement type, ExecutableElement factoryMethod, - TypeElementMembers members, ExecutableElement constructor) { + Stream> resolveConstructorProperties(TypeElement type, TypeElementMembers members, + ExecutableElement constructor) { Map> candidates = new LinkedHashMap<>(); constructor.getParameters().forEach((parameter) -> { String name = getParameterName(parameter); @@ -83,8 +82,8 @@ class PropertyDescriptorResolver { ExecutableElement getter = members.getPublicGetter(name, propertyType); ExecutableElement setter = members.getPublicSetter(name, propertyType); VariableElement field = members.getFields().get(name); - register(candidates, new ConstructorParameterPropertyDescriptor(type, factoryMethod, parameter, name, - propertyType, field, getter, setter)); + register(candidates, new ConstructorParameterPropertyDescriptor(type, null, parameter, name, propertyType, + field, getter, setter)); }); return candidates.values().stream(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java index f51290c654b..c1c4546a8a6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java @@ -38,15 +38,20 @@ import javax.lang.model.util.ElementFilter; * * @author Stephane Nicoll * @author Phillip Webb + * @author Moritz Halbritter */ class TypeElementMembers { private static final String OBJECT_CLASS_NAME = Object.class.getName(); + private static final String RECORD_CLASS_NAME = "java.lang.Record"; + private final MetadataGenerationEnvironment env; private final TypeElement targetType; + private final boolean isRecord; + private final Map fields = new LinkedHashMap<>(); private final Map> publicGetters = new LinkedHashMap<>(); @@ -56,18 +61,20 @@ class TypeElementMembers { TypeElementMembers(MetadataGenerationEnvironment env, TypeElement targetType) { this.env = env; this.targetType = targetType; + this.isRecord = RECORD_CLASS_NAME.equals(targetType.getSuperclass().toString()); process(targetType); } private void process(TypeElement element) { - for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) { - processMethod(method); - } for (VariableElement field : ElementFilter.fieldsIn(element.getEnclosedElements())) { processField(field); } + for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) { + processMethod(method); + } Element superType = this.env.getTypeUtils().asElement(element.getSuperclass()); - if (superType instanceof TypeElement && !OBJECT_CLASS_NAME.equals(superType.toString())) { + if (superType instanceof TypeElement && !OBJECT_CLASS_NAME.equals(superType.toString()) + && !RECORD_CLASS_NAME.equals(superType.toString())) { process((TypeElement) superType); } } @@ -122,12 +129,22 @@ class TypeElementMembers { } private boolean isGetter(ExecutableElement method) { + boolean hasParameters = !method.getParameters().isEmpty(); + boolean returnsVoid = TypeKind.VOID == method.getReturnType().getKind(); + if (hasParameters || returnsVoid) { + return false; + } String name = method.getSimpleName().toString(); - return ((name.startsWith("get") && name.length() > 3) || (name.startsWith("is") && name.length() > 2)) - && method.getParameters().isEmpty() && (TypeKind.VOID != method.getReturnType().getKind()); + if (this.isRecord && this.fields.containsKey(name)) { + return true; + } + return (name.startsWith("get") && name.length() > 3) || (name.startsWith("is") && name.length() > 2); } private boolean isSetter(ExecutableElement method) { + if (this.isRecord) { + return false; + } final String name = method.getSimpleName().toString(); return (name.startsWith("set") && name.length() > 3 && method.getParameters().size() == 1 && isSetterReturnType(method)); @@ -151,16 +168,29 @@ class TypeElementMembers { } private String getAccessorName(String methodName) { - String name = methodName.startsWith("is") ? methodName.substring(2) : methodName.substring(3); + if (this.isRecord) { + return methodName; + } + String name; + if (methodName.startsWith("is")) { + name = methodName.substring(2); + } + else if (methodName.startsWith("get")) { + name = methodName.substring(3); + } + else if (methodName.startsWith("set")) { + name = methodName.substring(3); + } + else { + throw new AssertionError("methodName must start with 'is', 'get' or 'set', was '" + methodName + "'"); + } name = Character.toLowerCase(name.charAt(0)) + name.substring(1); return name; } private void processField(VariableElement field) { String name = field.getSimpleName().toString(); - if (!this.fields.containsKey(name)) { - this.fields.put(name, field); - } + this.fields.putIfAbsent(name, field); } Map getFields() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index 8d4ff5caa3d..53ebd5f200f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -24,6 +24,7 @@ import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.recursive.RecursiveProperties; import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties; import org.springframework.boot.configurationsample.simple.DeprecatedFieldSingleProperty; +import org.springframework.boot.configurationsample.simple.DeprecatedRecord; import org.springframework.boot.configurationsample.simple.DeprecatedSingleProperty; import org.springframework.boot.configurationsample.simple.DescriptionProperties; import org.springframework.boot.configurationsample.simple.HierarchicalProperties; @@ -219,6 +220,16 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene .withNoDeprecation().fromSource(type)); } + @Test + void deprecatedPropertyOnRecord() { + Class type = DeprecatedRecord.class; + ConfigurationMetadata metadata = compile(type); + assertThat(metadata).has(Metadata.withGroup("deprecated-record").fromSource(type)); + assertThat(metadata).has(Metadata.withProperty("deprecated-record.alpha", String.class).fromSource(type) + .withDeprecation("some-reason", null)); + assertThat(metadata).has(Metadata.withProperty("deprecated-record.bravo", String.class).fromSource(type)); + } + @Test void typBoxing() { Class type = BoxingPojo.class; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedRecord.java new file mode 100644 index 00000000000..c5563ce52be --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedRecord.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 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.simple; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.DeprecatedConfigurationProperty; + +/** + * Configuration properties as record with deprecated property. + * @param alpha alpha property, deprecated + * @param bravo bravo property + * + * @author Moritz Halbritter + */ +@ConfigurationProperties("deprecated-record") +public record DeprecatedRecord(String alpha, String bravo) { + + @Deprecated + @DeprecatedConfigurationProperty(reason = "some-reason") + public String alpha() { + return this.alpha; + } + +}