Unwrap AOP proxies in configprops endpoint serialization

Update `ConfigurationPropertiesReportEndpoint` to unwrap AOP
proxies. Prior to this commit, the configprops endpoint serialized
AOP proxies directly, exposing the Advised interface getters
(targetSource, exposeProxy, preFiltered) into the response.

See gh-50273

Signed-off-by: zxuhan7 <zxuhan7@gmail.com>
This commit is contained in:
zxuhan7
2026-05-12 16:38:00 -07:00
committed by Phillip Webb
parent 328c65daea
commit 592e0b96a3
2 changed files with 169 additions and 11 deletions
@@ -25,8 +25,8 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
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 +53,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;
@@ -212,22 +214,57 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
private ContextConfigurationPropertiesDescriptor describeBeans(ObjectMapper mapper, ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values()
.stream()
.filter(beanFilterPredicate)
.collect(Collectors.toMap(ConfigurationPropertiesBean::getName,
(bean) -> describeBean(mapper, bean, showUnsanitized)));
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new LinkedHashMap<>();
beans.values().stream().filter(beanFilterPredicate).forEach((bean) -> {
ConfigurationPropertiesBeanDescriptor descriptor = describeBean(mapper, bean, showUnsanitized);
if (descriptor != null) {
descriptors.put(bean.getName(), descriptor);
}
});
return new ContextConfigurationPropertiesDescriptor(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null);
}
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean,
boolean showUnsanitized) {
String prefix = bean.getAnnotation().prefix();
Map<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
Map<String, Object> properties = sanitize(prefix, serialized, showUnsanitized);
Map<String, Object> inputs = getInputs(prefix, serialized, showUnsanitized);
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
return describeTargetBean(bean.getInstance(), (instance) -> {
String prefix = bean.getAnnotation().prefix();
Map<String, Object> serialized = safeSerialize(mapper, instance, prefix);
Map<String, Object> properties = sanitize(prefix, serialized, showUnsanitized);
Map<String, Object> inputs = getInputs(prefix, serialized, showUnsanitized);
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
});
}
private ConfigurationPropertiesBeanDescriptor describeTargetBean(Object bean,
Function<Object, ConfigurationPropertiesBeanDescriptor> actionWithTarget) {
TargetSource targetSource = null;
Object target = bean;
while (target instanceof Advised advised) {
try {
targetSource = advised.getTargetSource();
target = targetSource.getTarget();
}
catch (Exception ex) {
return null;
}
}
if (target != null) {
try {
return actionWithTarget.apply(target);
}
finally {
if (targetSource != null) {
try {
targetSource.releaseTarget(target);
}
catch (Exception ex) {
// ignore
}
}
}
}
return null;
}
/**
@@ -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,53 @@ 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 aopProxyWithUnresolvableTargetIsExcluded() {
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()).doesNotContainKey("foo");
});
}
@Test
void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -313,6 +364,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 +635,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 {