From 112e85507ccab5228a101be00d3ed9be7cae2012 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 09:43:50 +0200 Subject: [PATCH] Support AOT processing of Value Object with several constructors Previously, AOT processing failed on processing an immutable configuration properties that declare several constructors as the core framework infrastructure tries to resolve the "autowired" constructor to use, even if the custom code fragments are never going to use it. This commit workarounds the problem in maintenance releases until a proper fix is provided in the core framework. When AOT runs, a SmartInstantiationAwareBeanPostProcessor is added to the bean factory to provide the constructor to use. This implementation relies on the same algorithm that the binder uses at runtime. Closes gh-37283 --- ...BeanFactoryInitializationAotProcessor.java | 36 +++++++++++++++++ ...tiesBeanRegistrationAotProcessorTests.java | 40 +++++++++++++++++++ 2 files changed, 76 insertions(+) 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 3786fc3acad..41efc8cd917 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 @@ -16,14 +16,18 @@ package org.springframework.boot.context.properties; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.boot.context.properties.bind.BindConstructorProvider; import org.springframework.boot.context.properties.bind.BindMethod; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; @@ -43,6 +47,7 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessor implements Be @Override public ConfigurationPropertiesReflectionHintsContribution processAheadOfTime( ConfigurableListableBeanFactory beanFactory) { + beanFactory.addBeanPostProcessor(new BindConstructorAwareBeanPostProcessor(beanFactory)); String[] beanNames = beanFactory.getBeanNamesForAnnotation(ConfigurationProperties.class); List> bindables = new ArrayList<>(); for (String beanName : beanNames) { @@ -58,6 +63,37 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessor implements Be return (!bindables.isEmpty()) ? new ConfigurationPropertiesReflectionHintsContribution(bindables) : null; } + /** + * {@link SmartInstantiationAwareBeanPostProcessor} implementation to work around + * framework's constructor resolver for immutable configuration properties. + *

+ * Constructor binding supports multiple constructors as long as one is identified as + * the candidate for binding. Unfortunately, framework is not aware of such feature + * and attempts to resolve the autowired constructor to use. + */ + static class BindConstructorAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor { + + private final ConfigurableListableBeanFactory beanFactory; + + BindConstructorAwareBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) + throws BeansException { + BindMethod bindMethod = BindMethodAttribute.get(this.beanFactory, beanName); + if (bindMethod != null && bindMethod == BindMethod.VALUE_OBJECT) { + Constructor bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(beanClass, false); + if (bindConstructor != null) { + return new Constructor[] { bindConstructor }; + } + } + return null; + } + + } + static final class ConfigurationPropertiesReflectionHintsContribution implements BeanFactoryInitializationAotContribution { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java index cb1d826c1fd..3829bf9033d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessorTests.java @@ -100,6 +100,20 @@ class ConfigurationPropertiesBeanRegistrationAotProcessorTests { }); } + @Test + @CompileWithForkedClassLoader + void aotContributedInitializerBindsValueObjectWithSpecificConstructor() { + compile(createContext(ValueObjectSampleBeanWithSpecificConstructorConfiguration.class), (freshContext) -> { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(freshContext, "test.name=Hello", + "test.counter=30"); + freshContext.refresh(); + ValueObjectWithSpecificConstructorSampleBean bean = freshContext + .getBean(ValueObjectWithSpecificConstructorSampleBean.class); + assertThat(bean.name).isEqualTo("Hello"); + assertThat(bean.counter).isEqualTo(30); + }); + } + @Test @CompileWithForkedClassLoader void aotContributedInitializerBindsJavaBean() { @@ -193,6 +207,32 @@ class ConfigurationPropertiesBeanRegistrationAotProcessorTests { } + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(ValueObjectWithSpecificConstructorSampleBean.class) + static class ValueObjectSampleBeanWithSpecificConstructorConfiguration { + + } + + @ConfigurationProperties("test") + public static class ValueObjectWithSpecificConstructorSampleBean { + + @SuppressWarnings("unused") + private final String name; + + @SuppressWarnings("unused") + private final Integer counter; + + ValueObjectWithSpecificConstructorSampleBean(String name, Integer counter) { + this.name = name; + this.counter = counter; + } + + private ValueObjectWithSpecificConstructorSampleBean(String name) { + this(name, 42); + } + + } + @Configuration(proxyBeanMethods = false) @ConfigurationPropertiesScan(basePackageClasses = BScanConfiguration.class) static class ScanTestConfiguration {