From ea094ddba6c78ebdf1f013b75cda6a35626d4dbb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 20 Oct 2022 16:31:55 -0700 Subject: [PATCH] Fix package tangles caused by ConfigurationProperties hints Relocate `ConfigurationPropertiesReflectionHintsProcessor` and refactor it to be a general purpose `BindableRuntimeHintsRegistrar`. Prior to this commit, `ConfigurationPropertiesReflectionHintsProcessor` was used to declare binding hints for classes that were bound, but might be `@ConfigurationProperties`. By moving and renaming the class, it's now better aligned to the way it's used. Support for `@NestedConfigurationProperties` has been implemented by adding a `@Nestable` meta-annotation. This allow us to create the appropriate hints, without the `Binder` needing to be directly aware of the `@NestedConfigurationProperties` annotation. Closes gh-32815 --- .../freemarker/FreeMarkerRuntimeHints.java | 11 +- ...ttpMessageConvertersAutoConfiguration.java | 12 +- .../boot/SpringApplication.java | 12 +- .../ConfigDataPropertiesRuntimeHints.java | 5 +- ...BeanFactoryInitializationAotProcessor.java | 6 +- ...ionPropertiesReflectionHintsProcessor.java | 252 ---------------- .../NestedConfigurationProperty.java | 5 +- .../bind/BindableRuntimeHintsRegistrar.java | 270 ++++++++++++++++++ .../boot/context/properties/bind/Nested.java | 38 +++ 9 files changed, 328 insertions(+), 283 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesReflectionHintsProcessor.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Nested.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerRuntimeHints.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerRuntimeHints.java index 88429be2722..737504998f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerRuntimeHints.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerRuntimeHints.java @@ -16,22 +16,19 @@ package org.springframework.boot.autoconfigure.freemarker; -import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityProperties; -import org.springframework.boot.context.properties.ConfigurationPropertiesReflectionHintsProcessor; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; /** * {@link RuntimeHintsRegistrar} for FreeMarker support. * * @author Moritz Halbritter */ -class FreeMarkerRuntimeHints implements RuntimeHintsRegistrar { +class FreeMarkerRuntimeHints extends BindableRuntimeHintsRegistrar { - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - ConfigurationPropertiesReflectionHintsProcessor - .processConfigurationProperties(FreeMarkerTemplateAvailabilityProperties.class, hints.reflection()); + FreeMarkerRuntimeHints() { + super(FreeMarkerTemplateAvailabilityProperties.class); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java index fb345ce95bb..766650995da 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.http; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -31,7 +29,7 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConf import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration.NotReactiveWebApplicationCondition; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration; -import org.springframework.boot.context.properties.ConfigurationPropertiesReflectionHintsProcessor; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.web.servlet.server.Encoding; import org.springframework.context.annotation.Bean; @@ -102,12 +100,10 @@ public class HttpMessageConvertersAutoConfiguration { } - static class HttpMessageConvertersAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar { + static class HttpMessageConvertersAutoConfigurationRuntimeHints extends BindableRuntimeHintsRegistrar { - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - ConfigurationPropertiesReflectionHintsProcessor.processConfigurationProperties(Encoding.class, - hints.reflection()); + HttpMessageConvertersAutoConfigurationRuntimeHints() { + super(Encoding.class); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 4e671e9464f..f71fe1437c2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -36,8 +36,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aot.AotDetector; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -49,8 +47,8 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.boot.Banner.Mode; -import org.springframework.boot.context.properties.ConfigurationPropertiesReflectionHintsProcessor; import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.convert.ApplicationConversionService; @@ -1428,12 +1426,10 @@ public class SpringApplication { } - static class SpringApplicationRuntimeHints implements RuntimeHintsRegistrar { + static class SpringApplicationRuntimeHints extends BindableRuntimeHintsRegistrar { - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - ConfigurationPropertiesReflectionHintsProcessor.processConfigurationProperties(SpringApplication.class, - hints.reflection()); + SpringApplicationRuntimeHints() { + super(SpringApplication.class); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataPropertiesRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataPropertiesRuntimeHints.java index 3464ee128f3..5a54442a9ac 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataPropertiesRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataPropertiesRuntimeHints.java @@ -19,7 +19,7 @@ package org.springframework.boot.context.config; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.boot.context.properties.ConfigurationPropertiesReflectionHintsProcessor; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.util.ReflectionUtils; /** @@ -31,8 +31,7 @@ class ConfigDataPropertiesRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - ConfigurationPropertiesReflectionHintsProcessor.processConfigurationProperties(ConfigDataProperties.class, - hints.reflection()); + BindableRuntimeHintsRegistrar.forTypes(ConfigDataProperties.class).registerHints(hints); hints.reflection().registerMethod(ReflectionUtils.findMethod(ConfigDataLocation.class, "of", String.class), ExecutableMode.INVOKE); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java index 4e84f769670..d00b6daeecf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContrib import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -66,10 +67,7 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessor implements Be @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - for (Class type : this.types) { - ConfigurationPropertiesReflectionHintsProcessor.processConfigurationProperties(type, - generationContext.getRuntimeHints().reflection()); - } + BindableRuntimeHintsRegistrar.forTypes(this.types).registerHints(generationContext.getRuntimeHints()); } } 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 deleted file mode 100644 index deaed1726ff..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesReflectionHintsProcessor.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2012-2022 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.context.properties; - -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.aot.hint.ExecutableMode; -import org.springframework.aot.hint.ReflectionHints; -import org.springframework.beans.BeanInfoFactory; -import org.springframework.beans.ExtendedBeanInfoFactory; -import org.springframework.boot.context.properties.bind.BindConstructorProvider; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.util.ReflectionUtils; - -/** - * Registers a given type on {@link ReflectionHints} for binding purposes, discovering any - * nested type it may expose via a property. - * - * @author Andy Wilkinson - * @author Moritz Halbritter - * @author Sebastien Deleuze - * @since 3.0.0 - */ -public final class ConfigurationPropertiesReflectionHintsProcessor { - - private static final BeanInfoFactory beanInfoFactory = new ExtendedBeanInfoFactory(); - - private final Class type; - - private final Constructor bindConstructor; - - private final BeanInfo beanInfo; - - private final Set> seen; - - private ConfigurationPropertiesReflectionHintsProcessor(Class type, Constructor bindConstructor, - Set> seen) { - this.type = type; - this.bindConstructor = bindConstructor; - this.beanInfo = getBeanInfo(type); - this.seen = seen; - } - - /** - * Registers a given type on {@link ReflectionHints} for binding purposes, discovering - * any nested type it may expose via a property. - * @param type type to process - * @param reflectionHints {@link ReflectionHints} to register the types on - */ - public static void processConfigurationProperties(Class type, ReflectionHints reflectionHints) { - new ConfigurationPropertiesReflectionHintsProcessor(type, getBindConstructor(type, false), new HashSet<>()) - .process(reflectionHints); - } - - private void processNestedType(Class type, ReflectionHints reflectionHints) { - processNestedType(type, getBindConstructor(type, true), reflectionHints); - } - - private void processNestedType(Class type, Constructor bindConstructor, ReflectionHints reflectionHints) { - new ConfigurationPropertiesReflectionHintsProcessor(type, bindConstructor, this.seen).process(reflectionHints); - } - - private static Constructor getBindConstructor(Class type, boolean nestedType) { - Bindable bindable = Bindable.of(type); - return BindConstructorProvider.DEFAULT.getBindConstructor(bindable, nestedType); - } - - private void process(ReflectionHints reflectionHints) { - if (this.seen.contains(this.type)) { - return; - } - this.seen.add(this.type); - handleConstructor(reflectionHints); - if (this.bindConstructor != null) { - handleValueObjectProperties(reflectionHints); - } - else if (this.beanInfo != null) { - handleJavaBeanProperties(reflectionHints); - } - } - - private void handleConstructor(ReflectionHints reflectionHints) { - if (this.bindConstructor != null) { - reflectionHints.registerConstructor(this.bindConstructor, ExecutableMode.INVOKE); - return; - } - Arrays.stream(this.type.getDeclaredConstructors()).filter(this::hasNoParameters).findFirst() - .ifPresent((constructor) -> reflectionHints.registerConstructor(constructor, ExecutableMode.INVOKE)); - } - - private boolean hasNoParameters(Constructor candidate) { - return candidate.getParameterCount() == 0; - } - - private void handleValueObjectProperties(ReflectionHints reflectionHints) { - for (int i = 0; i < this.bindConstructor.getParameterCount(); i++) { - String propertyName = this.bindConstructor.getParameters()[i].getName(); - ResolvableType propertyType = ResolvableType.forConstructorParameter(this.bindConstructor, i); - handleProperty(reflectionHints, propertyName, propertyType); - } - } - - private void handleJavaBeanProperties(ReflectionHints reflectionHints) { - for (PropertyDescriptor propertyDescriptor : this.beanInfo.getPropertyDescriptors()) { - Method writeMethod = propertyDescriptor.getWriteMethod(); - if (writeMethod != null) { - reflectionHints.registerMethod(writeMethod, ExecutableMode.INVOKE); - } - Method readMethod = propertyDescriptor.getReadMethod(); - if (readMethod != null) { - ResolvableType propertyType = ResolvableType.forMethodReturnType(readMethod, this.type); - String propertyName = propertyDescriptor.getName(); - if (isSetterMandatory(propertyName, propertyType) && writeMethod == null) { - continue; - } - handleProperty(reflectionHints, propertyName, propertyType); - reflectionHints.registerMethod(readMethod, ExecutableMode.INVOKE); - } - } - } - - private boolean isSetterMandatory(String propertyName, ResolvableType propertyType) { - Class propertyClass = propertyType.resolve(); - if (propertyClass == null) { - return true; - } - if (isContainer(propertyType)) { - return false; - } - return !isNestedType(propertyName, propertyClass); - } - - private void handleProperty(ReflectionHints reflectionHints, String propertyName, ResolvableType propertyType) { - Class propertyClass = propertyType.resolve(); - if (propertyClass == null) { - return; - } - if (propertyClass.equals(this.type)) { - return; // Prevent infinite recursion - } - Class componentType = getComponentClass(propertyType); - if (componentType != null) { - // Can be a list of simple types - if (!isJavaType(componentType)) { - processNestedType(componentType, reflectionHints); - } - } - else if (isNestedType(propertyName, propertyClass)) { - processNestedType(propertyClass, reflectionHints); - } - } - - private Class getComponentClass(ResolvableType type) { - ResolvableType componentType = getComponentType(type); - if (componentType == null) { - return null; - } - if (isContainer(componentType)) { - // Resolve nested generics like Map> - return getComponentClass(componentType); - } - return componentType.toClass(); - } - - private ResolvableType getComponentType(ResolvableType type) { - if (type.isArray()) { - return type.getComponentType(); - } - if (isCollection(type)) { - return type.asCollection().getGeneric(); - } - if (isMap(type)) { - return type.asMap().getGeneric(1); - } - return null; - } - - private boolean isContainer(ResolvableType type) { - return type.isArray() || isCollection(type) || isMap(type); - } - - private boolean isCollection(ResolvableType type) { - return Collection.class.isAssignableFrom(type.toClass()); - } - - private boolean isMap(ResolvableType type) { - return Map.class.isAssignableFrom(type.toClass()); - } - - /** - * Specify whether the specified property refer to a nested type. A nested type - * 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 - */ - private boolean isNestedType(String propertyName, Class propertyType) { - if (this.type.equals(propertyType.getDeclaringClass())) { - return true; - } - else { - Field field = ReflectionUtils.findField(this.type, propertyName); - return field != null && MergedAnnotations.from(field).isPresent(NestedConfigurationProperty.class); - } - } - - private boolean isJavaType(Class candidate) { - return candidate.getPackageName().startsWith("java."); - } - - private static BeanInfo getBeanInfo(Class beanType) { - try { - BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanType); - if (beanInfo != null) { - return beanInfo; - } - return Introspector.getBeanInfo(beanType, Introspector.IGNORE_ALL_BEANINFO); - } - catch (IntrospectionException ex) { - return null; - } - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java index 9beee6c1837..274aeb993ff 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 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. @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.context.properties.bind.Nested; + /** * Indicates that a field in a {@link ConfigurationProperties @ConfigurationProperties} * object should be treated as if it were a nested type. This annotation has no bearing on @@ -39,6 +41,7 @@ import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented +@Nested public @interface NestedConfigurationProperty { } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java new file mode 100644 index 00000000000..09814a2641e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindableRuntimeHintsRegistrar.java @@ -0,0 +1,270 @@ +/* + * Copyright 2012-2022 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.context.properties.bind; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.StreamSupport; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.BeanInfoFactory; +import org.springframework.beans.ExtendedBeanInfoFactory; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * {@link RuntimeHintsRegistrar} that can be used to register {@link ReflectionHints} for + * {@link Bindable} types, discovering any nested type it may expose via a property. + *

+ * This class can be used as a base-class, or instantiated using the {@code forTypes} + * factory methods. + * + * @author Andy Wilkinson + * @author Moritz Halbritter + * @author Sebastien Deleuze + * @author Phillip Webb + * @since 3.0.0 + */ +public class BindableRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + private static final BeanInfoFactory beanInfoFactory = new ExtendedBeanInfoFactory(); + + private final Class[] types; + + protected BindableRuntimeHintsRegistrar(Class... types) { + this.types = types; + } + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + registerHints(hints); + } + + public void registerHints(RuntimeHints hints) { + for (Class type : this.types) { + new Processor(type).process(hints.reflection()); + } + } + + public static BindableRuntimeHintsRegistrar forTypes(Iterable> types) { + Assert.notNull(types, "Types must not be null"); + return forTypes(StreamSupport.stream(types.spliterator(), false).toArray(Class[]::new)); + } + + public static BindableRuntimeHintsRegistrar forTypes(Class... types) { + return new BindableRuntimeHintsRegistrar(types); + } + + private final class Processor { + + private final Class type; + + private final Constructor bindConstructor; + + private final BeanInfo beanInfo; + + private final Set> seen; + + Processor(Class type) { + this(type, false, new HashSet<>()); + } + + private Processor(Class type, boolean nestedType, Set> seen) { + this.type = type; + this.bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(Bindable.of(type), nestedType); + this.beanInfo = getBeanInfo(type); + this.seen = seen; + } + + private static BeanInfo getBeanInfo(Class beanType) { + try { + BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanType); + if (beanInfo != null) { + return beanInfo; + } + return Introspector.getBeanInfo(beanType, Introspector.IGNORE_ALL_BEANINFO); + } + catch (IntrospectionException ex) { + return null; + } + } + + void process(ReflectionHints hints) { + if (this.seen.contains(this.type)) { + return; + } + this.seen.add(this.type); + handleConstructor(hints); + if (this.bindConstructor != null) { + handleValueObjectProperties(hints); + } + else if (this.beanInfo != null) { + handleJavaBeanProperties(hints); + } + } + + private void handleConstructor(ReflectionHints hints) { + if (this.bindConstructor != null) { + hints.registerConstructor(this.bindConstructor, ExecutableMode.INVOKE); + return; + } + Arrays.stream(this.type.getDeclaredConstructors()).filter(this::hasNoParameters).findFirst() + .ifPresent((constructor) -> hints.registerConstructor(constructor, ExecutableMode.INVOKE)); + } + + private boolean hasNoParameters(Constructor candidate) { + return candidate.getParameterCount() == 0; + } + + private void handleValueObjectProperties(ReflectionHints hints) { + for (int i = 0; i < this.bindConstructor.getParameterCount(); i++) { + String propertyName = this.bindConstructor.getParameters()[i].getName(); + ResolvableType propertyType = ResolvableType.forConstructorParameter(this.bindConstructor, i); + handleProperty(hints, propertyName, propertyType); + } + } + + private void handleJavaBeanProperties(ReflectionHints hints) { + for (PropertyDescriptor propertyDescriptor : this.beanInfo.getPropertyDescriptors()) { + Method writeMethod = propertyDescriptor.getWriteMethod(); + if (writeMethod != null) { + hints.registerMethod(writeMethod, ExecutableMode.INVOKE); + } + Method readMethod = propertyDescriptor.getReadMethod(); + if (readMethod != null) { + ResolvableType propertyType = ResolvableType.forMethodReturnType(readMethod, this.type); + String propertyName = propertyDescriptor.getName(); + if (isSetterMandatory(propertyName, propertyType) && writeMethod == null) { + continue; + } + handleProperty(hints, propertyName, propertyType); + hints.registerMethod(readMethod, ExecutableMode.INVOKE); + } + } + } + + private boolean isSetterMandatory(String propertyName, ResolvableType propertyType) { + Class propertyClass = propertyType.resolve(); + if (propertyClass == null) { + return true; + } + if (isContainer(propertyType)) { + return false; + } + return !isNestedType(propertyName, propertyClass); + } + + private void handleProperty(ReflectionHints hints, String propertyName, ResolvableType propertyType) { + Class propertyClass = propertyType.resolve(); + if (propertyClass == null) { + return; + } + if (propertyClass.equals(this.type)) { + return; // Prevent infinite recursion + } + Class componentType = getComponentClass(propertyType); + if (componentType != null) { + // Can be a list of simple types + if (!isJavaType(componentType)) { + processNested(componentType, hints); + } + } + else if (isNestedType(propertyName, propertyClass)) { + processNested(propertyClass, hints); + } + } + + private void processNested(Class type, ReflectionHints hints) { + new Processor(type, true, this.seen).process(hints); + } + + private Class getComponentClass(ResolvableType type) { + ResolvableType componentType = getComponentType(type); + if (componentType == null) { + return null; + } + if (isContainer(componentType)) { + // Resolve nested generics like Map> + return getComponentClass(componentType); + } + return componentType.toClass(); + } + + private ResolvableType getComponentType(ResolvableType type) { + if (type.isArray()) { + return type.getComponentType(); + } + if (isCollection(type)) { + return type.asCollection().getGeneric(); + } + if (isMap(type)) { + return type.asMap().getGeneric(1); + } + return null; + } + + private boolean isContainer(ResolvableType type) { + return type.isArray() || isCollection(type) || isMap(type); + } + + private boolean isCollection(ResolvableType type) { + return Collection.class.isAssignableFrom(type.toClass()); + } + + private boolean isMap(ResolvableType type) { + return Map.class.isAssignableFrom(type.toClass()); + } + + /** + * Specify whether the specified property refer to a nested type. A nested type + * 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 + */ + private boolean isNestedType(String propertyName, Class propertyType) { + if (this.type.equals(propertyType.getDeclaringClass())) { + return true; + } + Field field = ReflectionUtils.findField(this.type, propertyName); + return (field != null) && MergedAnnotations.from(field).isPresent(Nested.class); + } + + private boolean isJavaType(Class candidate) { + return candidate.getPackageName().startsWith("java."); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Nested.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Nested.java new file mode 100644 index 00000000000..a352c9423b2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Nested.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2022 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.context.properties.bind; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta-annotation that should be added to annotations that indicate a field is a nested + * type. Used to ensure that correct reflection hints are registered. + * + * @author Phillip Webb + * @since 3.0.0 + * @see BindableRuntimeHintsRegistrar + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +@Documented +public @interface Nested { + +}