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 {