From 6c44055fd4b80fdef23cf0f08292f44434ff62f5 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 18 Jan 2023 12:01:06 +0100 Subject: [PATCH 1/2] Polish PropertyDescriptorResolver --- .../PropertyDescriptorResolver.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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 084e61f2bc0..d1a9c4c2bae 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 @@ -58,23 +58,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); @@ -82,8 +81,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(); } From 26d658802ff5fd1a0eac49317b5aac132bdb02f3 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 18 Jan 2023 12:02:34 +0100 Subject: [PATCH 2/2] Add support for record accessors in spring-boot-configuration-processor Closes gh-29526 --- .../TypeElementMembers.java | 50 +++++++++++++++---- ...ationMetadataAnnotationProcessorTests.java | 22 ++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) 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 e9825a23688..5c26fb979aa 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 @@ -223,6 +223,28 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene .withNoDeprecation().fromSource(type)); } + @Test + @EnabledForJreRange(min = JRE.JAVA_16) + void deprecatedPropertyOnRecord(@TempDir File temp) throws IOException { + File exampleRecord = new File(temp, "DeprecatedRecord.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) { + writer.println("@org.springframework.boot.configurationsample.ConstructorBinding"); + writer.println( + "@org.springframework.boot.configurationsample.ConfigurationProperties(\"deprecated-record\")"); + writer.println("public record DeprecatedRecord(String alpha, String bravo) {"); + writer.println("@java.lang.Deprecated"); + writer.println( + "@org.springframework.boot.configurationsample.DeprecatedConfigurationProperty(reason = \"some-reason\")"); + writer.println("public String alpha() { return this.alpha; }"); + writer.println("}"); + } + ConfigurationMetadata metadata = compile(exampleRecord); + assertThat(metadata).has(Metadata.withGroup("deprecated-record")); + assertThat(metadata).has( + Metadata.withProperty("deprecated-record.alpha", String.class).withDeprecation("some-reason", null)); + assertThat(metadata).has(Metadata.withProperty("deprecated-record.bravo", String.class)); + } + @Test void typBoxing() { Class type = BoxingPojo.class;