Commit b240c810 authored by Andy Wilkinson's avatar Andy Wilkinson

Fix @ConfigurationProperties on @Bean methods without metadata caching

Due to a current limitation of Spring Framework, when bean metadata
caching is disabled, a merged bean definition may have a null
resolved factory method that would have been non-null if bean metadata
caching was enabled. Configuration property binding for @Bean methods
annotated with @ConfigurationProperties relied upon the resolved
factory method being enabled to find the @ConfigurationProperties
annotation and trigger property binding. As a result, when bean
metadata caching is disabled on the bean factory, such
@ConfigurationProperties beans would not be bound.

This commit works around the limitation by adding a fallback that
performs a reflection-based search for the factory method when the
resolved factory method on the bean definition is null. This allows
the bean's factory method and any @ConfigurationProperties annotation
on it to be found, ensuring that propoerty binding is then performed.

Fixes gh-18440
parent 3dac8e9a
...@@ -22,6 +22,7 @@ import java.lang.reflect.Method; ...@@ -22,6 +22,7 @@ import java.lang.reflect.Method;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
...@@ -41,6 +42,8 @@ import org.springframework.core.annotation.MergedAnnotation; ...@@ -41,6 +42,8 @@ import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
/** /**
...@@ -196,11 +199,36 @@ public final class ConfigurationPropertiesBean { ...@@ -196,11 +199,36 @@ public final class ConfigurationPropertiesBean {
if (beanFactory.containsBeanDefinition(beanName)) { if (beanFactory.containsBeanDefinition(beanName)) {
BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
if (beanDefinition instanceof RootBeanDefinition) { if (beanDefinition instanceof RootBeanDefinition) {
return ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod(); Method resolvedFactoryMethod = ((RootBeanDefinition) beanDefinition).getResolvedFactoryMethod();
if (resolvedFactoryMethod != null) {
return resolvedFactoryMethod;
} }
} }
return findFactoryMethodUsingReflection(beanFactory, beanDefinition);
}
return null;
}
private static Method findFactoryMethodUsingReflection(ConfigurableListableBeanFactory beanFactory,
BeanDefinition beanDefinition) {
String factoryMethodName = beanDefinition.getFactoryMethodName();
String factoryBeanName = beanDefinition.getFactoryBeanName();
if (factoryMethodName == null || factoryBeanName == null) {
return null; return null;
} }
System.out.println("***** " + beanDefinition.getFactoryMethodName());
Class<?> factoryType = beanFactory.getType(factoryBeanName);
if (factoryType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
factoryType = factoryType.getSuperclass();
}
AtomicReference<Method> factoryMethod = new AtomicReference<>();
ReflectionUtils.doWithMethods(factoryType, (method) -> {
if (method.getName().equals(factoryMethodName)) {
factoryMethod.set(method);
}
});
return factoryMethod.get();
}
static ConfigurationPropertiesBean forValueObject(Class<?> beanClass, String beanName) { static ConfigurationPropertiesBean forValueObject(Class<?> beanClass, String beanName) {
ConfigurationPropertiesBean propertiesBean = create(beanName, null, beanClass, null); ConfigurationPropertiesBean propertiesBean = create(beanName, null, beanClass, null);
......
...@@ -27,7 +27,10 @@ import org.springframework.boot.context.properties.bind.Bindable; ...@@ -27,7 +27,10 @@ import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
...@@ -99,6 +102,31 @@ class ConfigurationPropertiesBeanTests { ...@@ -99,6 +102,31 @@ class ConfigurationPropertiesBeanTests {
}); });
} }
@Test
void getWhenImportedFactoryMethodIsAnnotatedAndMetadataCachingIsDisabledReturnsBean() throws Throwable {
getWithoutBeanMetadataCaching(NonAnnotatedBeanImportConfiguration.class, "nonAnnotatedBean",
(propertiesBean) -> {
assertThat(propertiesBean).isNotNull();
assertThat(propertiesBean.getName()).isEqualTo("nonAnnotatedBean");
assertThat(propertiesBean.getInstance()).isInstanceOf(NonAnnotatedBean.class);
assertThat(propertiesBean.getType()).isEqualTo(NonAnnotatedBean.class);
assertThat(propertiesBean.getAnnotation().prefix()).isEqualTo("prefix");
assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.JAVA_BEAN);
});
}
@Test
void getWhenImportedFactoryMethodIsAnnotatedReturnsBean() throws Throwable {
get(NonAnnotatedBeanImportConfiguration.class, "nonAnnotatedBean", (propertiesBean) -> {
assertThat(propertiesBean).isNotNull();
assertThat(propertiesBean.getName()).isEqualTo("nonAnnotatedBean");
assertThat(propertiesBean.getInstance()).isInstanceOf(NonAnnotatedBean.class);
assertThat(propertiesBean.getType()).isEqualTo(NonAnnotatedBean.class);
assertThat(propertiesBean.getAnnotation().prefix()).isEqualTo("prefix");
assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.JAVA_BEAN);
});
}
@Test @Test
void getWhenHasFactoryMethodBindsUsingMethodReturnType() throws Throwable { void getWhenHasFactoryMethodBindsUsingMethodReturnType() throws Throwable {
get(NonAnnotatedGenericBeanConfiguration.class, "nonAnnotatedGenericBean", (propertiesBean) -> { get(NonAnnotatedGenericBeanConfiguration.class, "nonAnnotatedGenericBean", (propertiesBean) -> {
...@@ -218,7 +246,20 @@ class ConfigurationPropertiesBeanTests { ...@@ -218,7 +246,20 @@ class ConfigurationPropertiesBeanTests {
private void get(Class<?> configuration, String beanName, ThrowingConsumer<ConfigurationPropertiesBean> consumer) private void get(Class<?> configuration, String beanName, ThrowingConsumer<ConfigurationPropertiesBean> consumer)
throws Throwable { throws Throwable {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configuration)) { get(configuration, beanName, true, consumer);
}
private void getWithoutBeanMetadataCaching(Class<?> configuration, String beanName,
ThrowingConsumer<ConfigurationPropertiesBean> consumer) throws Throwable {
get(configuration, beanName, false, consumer);
}
private void get(Class<?> configuration, String beanName, boolean cacheBeanMetadata,
ThrowingConsumer<ConfigurationPropertiesBean> consumer) throws Throwable {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.getBeanFactory().setCacheBeanMetadata(cacheBeanMetadata);
context.register(configuration);
context.refresh();
Object bean = context.getBean(beanName); Object bean = context.getBean(beanName);
consumer.accept(ConfigurationPropertiesBean.get(context, bean, beanName)); consumer.accept(ConfigurationPropertiesBean.get(context, bean, beanName));
} }
...@@ -404,4 +445,19 @@ class ConfigurationPropertiesBeanTests { ...@@ -404,4 +445,19 @@ class ConfigurationPropertiesBeanTests {
} }
@Configuration(proxyBeanMethods = false)
@Import(NonAnnotatedBeanConfigurationImportSelector.class)
static class NonAnnotatedBeanImportConfiguration {
}
static class NonAnnotatedBeanConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] { NonAnnotatedBeanConfiguration.class.getName() };
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment