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
This commit is contained in:
Stephane Nicoll 2023-09-11 09:43:50 +02:00
parent 994bafdfd9
commit 112e85507c
2 changed files with 76 additions and 0 deletions

View File

@ -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<Bindable<?>> 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.
* <p>
* 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 {

View File

@ -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 {