From 35c49afd97dd353020b96df9c537bb44d819ef7e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 21 Jul 2022 15:51:46 +0200 Subject: [PATCH] Generate hints for nested generics in configuration properties See gh-31708 --- ...ionPropertiesReflectionHintsProcessor.java | 34 +++++-- ...actoryInitializationAotProcessorTests.java | 88 +++++++++++++++++++ 2 files changed, 116 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesReflectionHintsProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesReflectionHintsProcessor.java index 797456c028..7cfab17c67 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesReflectionHintsProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesReflectionHintsProcessor.java @@ -163,7 +163,7 @@ public final class ConfigurationPropertiesReflectionHintsProcessor { if (propertyClass == null) { return true; } - if (getComponentType(propertyType) != null) { + if (isContainer(propertyType)) { return false; } return !isNestedType(propertyName, propertyClass); @@ -171,21 +171,43 @@ public final class ConfigurationPropertiesReflectionHintsProcessor { private Class getComponentType(ResolvableType propertyType) { Class propertyClass = propertyType.toClass(); + ResolvableType componentType = null; if (propertyType.isArray()) { - return propertyType.getComponentType().toClass(); + componentType = propertyType.getComponentType(); } else if (Collection.class.isAssignableFrom(propertyClass)) { - return propertyType.as(Collection.class).getGeneric(0).toClass(); + componentType = propertyType.asCollection().getGeneric(0); } else if (Map.class.isAssignableFrom(propertyClass)) { - return propertyType.as(Map.class).getGeneric(1).toClass(); + componentType = propertyType.asMap().getGeneric(1); } - return null; + if (componentType == null) { + return null; + } + if (isContainer(componentType)) { + // Resolve nested generics like Map> + return getComponentType(componentType); + } + return componentType.toClass(); + } + + private boolean isContainer(ResolvableType type) { + if (type.isArray()) { + return true; + } + if (Collection.class.isAssignableFrom(type.toClass())) { + return true; + } + else if (Map.class.isAssignableFrom(type.toClass())) { + return true; + } + return false; } /** * Specify whether the specified property refer to a nested type. A nested type - * represents a sub-namespace that need to be fully resolved. + * represents a sub-namespace that need to be fully resolved. Nested types are either + * inner classes or annotated with {@link NestedConfigurationProperty}. * @param propertyName the name of the property * @param propertyType the type of the property * @return whether the specified {@code propertyType} is a nested type diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests.java index 5bbabde9ec..7e75f3ff50 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -34,6 +35,7 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.factory.aot.AotFactoriesLoader; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; @@ -53,6 +55,7 @@ import static org.mockito.Mockito.mock; * Tests for {@link ConfigurationPropertiesBeanFactoryInitializationAotProcessor}. * * @author Stephane Nicoll + * @author Moritz Halbritter */ class ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests { @@ -227,6 +230,31 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests { .anySatisfy(javaBeanBinding(GenericObject.class)); } + @Test + void processConfigurationPropertiesWithNestedGenerics() { + RuntimeHints runtimeHints = process(NestedGenerics.class); + assertThat(RuntimeHintsPredicates.reflection().onType(NestedGenerics.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)) + .accepts(runtimeHints); + assertThat(RuntimeHintsPredicates.reflection().onType(NestedGenerics.Nested.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)) + .accepts(runtimeHints); + } + + @Test + void processConfigurationPropertiesWithMultipleNestedClasses() { + RuntimeHints runtimeHints = process(TripleNested.class); + assertThat(RuntimeHintsPredicates.reflection().onType(TripleNested.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)) + .accepts(runtimeHints); + assertThat(RuntimeHintsPredicates.reflection().onType(TripleNested.DoubleNested.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)) + .accepts(runtimeHints); + assertThat(RuntimeHintsPredicates.reflection().onType(TripleNested.DoubleNested.Nested.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS)) + .accepts(runtimeHints); + } + private Consumer javaBeanBinding(Class type) { return javaBeanBinding(type, type.getDeclaredConstructors()[0]); } @@ -590,4 +618,64 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessorTests { } + @ConfigurationProperties(prefix = "nested-generics") + public static class NestedGenerics { + + private final Map> nested = new HashMap<>(); + + public Map> getNested() { + return this.nested; + } + + public static class Nested { + + private String field; + + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + } + + } + + } + + @ConfigurationProperties(prefix = "triple-nested") + public static class TripleNested { + + private final DoubleNested doubleNested = new DoubleNested(); + + public DoubleNested getDoubleNested() { + return this.doubleNested; + } + + public static class DoubleNested { + + private final Nested nested = new Nested(); + + public Nested getNested() { + return this.nested; + } + + public static class Nested { + + private String field; + + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + } + + } + + } + + } + }