Merge pull request #50273 from zxuhan

Closes gh-50273

* pr/50273:
  Polish "Unwrap AOP proxies in configprops endpoint serialization"
  Unwrap AOP proxies in configprops endpoint serialization
This commit is contained in:
Phillip Webb
2026-05-12 16:57:43 -07:00
2 changed files with 144 additions and 8 deletions
@@ -26,7 +26,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
@@ -53,6 +52,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.SanitizableData;
@@ -211,14 +212,14 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
private ContextConfigurationPropertiesDescriptor describeBeans(ObjectMapper mapper, ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
ApplicationContext parent = context.getParent();
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values()
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new LinkedHashMap<>();
beans.values()
.stream()
.filter(beanFilterPredicate)
.collect(Collectors.toMap(ConfigurationPropertiesBean::getName,
(bean) -> describeBean(mapper, bean, showUnsanitized)));
return new ContextConfigurationPropertiesDescriptor(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null);
.forEach((bean) -> descriptors.put(bean.getName(), describeBean(mapper, bean, showUnsanitized)));
return new ContextConfigurationPropertiesDescriptor(descriptors, (parent != null) ? parent.getId() : null);
}
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean,
@@ -231,8 +232,8 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
}
/**
* Cautiously serialize the bean to a map (returning a map with an error message
* instead of throwing an exception if there is a problem).
* Cautiously serialize the ultimate bean target to a map (returning a map with an
* error message instead of throwing an exception if there is a problem).
* @param mapper the object mapper
* @param bean the source bean
* @param prefix the prefix
@@ -241,6 +242,18 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
@SuppressWarnings({ "unchecked" })
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, String prefix) {
try {
if (bean instanceof Advised advised) {
TargetSource targetSource = advised.getTargetSource();
Object target = targetSource.getTarget();
try {
return safeSerialize(mapper, target, prefix);
}
finally {
if (target != null) {
targetSource.releaseTarget(target);
}
}
}
return new HashMap<>(mapper.convertValue(bean, Map.class));
}
catch (Exception ex) {
@@ -29,12 +29,16 @@ import java.util.Map;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationPropertiesDescriptor;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -253,6 +257,55 @@ class ConfigurationPropertiesReportEndpointSerializationTests {
});
}
@Test
void aopProxyDoesNotLeakAdvisedInternals() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(CglibProxiedFooConfig.class)
.withPropertyValues("foo.name:test");
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class);
ConfigurationPropertiesDescriptor applicationProperties = endpoint.configurationProperties();
ConfigurationPropertiesBeanDescriptor foo = getContextDescriptor(context, applicationProperties).getBeans()
.get("foo");
assertThat(foo).isNotNull();
Map<String, Object> map = foo.getProperties();
assertThat(map).containsOnlyKeys("name", "bar");
assertThat(map).containsEntry("name", "test");
});
}
@Test
void aopProxyTargetingAnotherProxyIsUnwrapped() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(NestedCglibProxiedFooConfig.class)
.withPropertyValues("foo.name:nested");
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class);
ConfigurationPropertiesDescriptor applicationProperties = endpoint.configurationProperties();
ConfigurationPropertiesBeanDescriptor foo = getContextDescriptor(context, applicationProperties).getBeans()
.get("foo");
assertThat(foo).isNotNull();
assertThat(foo.getProperties()).containsOnlyKeys("name", "bar");
assertThat(foo.getProperties()).containsEntry("name", "nested");
});
}
@Test
void aopProxyWithUnresolvableTarget() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(UnresolvableTargetFooConfig.class);
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class);
ConfigurationPropertiesDescriptor applicationProperties = endpoint.configurationProperties();
assertThat(getContextDescriptor(context, applicationProperties).getBeans()).containsKey("foo")
.satisfies((beans) -> assertThat(beans.get("foo").getProperties()).containsEntry("error",
"Cannot serialize 'foo'"));
});
}
@Test
void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -313,6 +366,14 @@ class ConfigurationPropertiesReportEndpointSerializationTests {
});
}
private ContextConfigurationPropertiesDescriptor getContextDescriptor(AssertableApplicationContext context,
ConfigurationPropertiesDescriptor applicationProperties) {
ContextConfigurationPropertiesDescriptor contextDescriptor = applicationProperties.getContexts()
.get(context.getId());
assertThat(contextDescriptor).isNotNull();
return contextDescriptor;
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
static class Base {
@@ -576,6 +637,68 @@ class ConfigurationPropertiesReportEndpointSerializationTests {
}
@Configuration(proxyBeanMethods = false)
@Import(Base.class)
static class CglibProxiedFooConfig {
@Bean
@ConfigurationProperties("foo")
Foo foo() {
ProxyFactory proxyFactory = new ProxyFactory(new Foo());
proxyFactory.setProxyTargetClass(true);
return (Foo) proxyFactory.getProxy();
}
}
@Configuration(proxyBeanMethods = false)
@Import(Base.class)
static class NestedCglibProxiedFooConfig {
@Bean
@ConfigurationProperties("foo")
Foo foo() {
ProxyFactory inner = new ProxyFactory(new Foo());
inner.setProxyTargetClass(true);
ProxyFactory outer = new ProxyFactory(inner.getProxy());
outer.setProxyTargetClass(true);
return (Foo) outer.getProxy();
}
}
@Configuration(proxyBeanMethods = false)
@Import(Base.class)
static class UnresolvableTargetFooConfig {
@Bean
@ConfigurationProperties("foo")
Foo foo() {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setProxyTargetClass(true);
proxyFactory.setTargetSource(new TargetSource() {
@Override
public Class<?> getTargetClass() {
return Foo.class;
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() throws Exception {
throw new IllegalStateException("no target");
}
});
return (Foo) proxyFactory.getProxy();
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
static class HikariDataSourceConfig {