From 592e0b96a310515f832ee8b7a88a2ec73bb0020b Mon Sep 17 00:00:00 2001 From: zxuhan7 Date: Fri, 1 May 2026 12:38:50 +0200 Subject: [PATCH 1/2] 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 --- ...ConfigurationPropertiesReportEndpoint.java | 59 +++++++-- ...rtiesReportEndpointSerializationTests.java | 121 ++++++++++++++++++ 2 files changed, 169 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index f6b90d84985..caaaa8ce49f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -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 beanFilterPredicate, boolean showUnsanitized) { Map beans = ConfigurationPropertiesBean.getAll(context); - Map descriptors = beans.values() - .stream() - .filter(beanFilterPredicate) - .collect(Collectors.toMap(ConfigurationPropertiesBean::getName, - (bean) -> describeBean(mapper, bean, showUnsanitized))); + Map 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 serialized = safeSerialize(mapper, bean.getInstance(), prefix); - Map properties = sanitize(prefix, serialized, showUnsanitized); - Map inputs = getInputs(prefix, serialized, showUnsanitized); - return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); + return describeTargetBean(bean.getInstance(), (instance) -> { + String prefix = bean.getAnnotation().prefix(); + Map serialized = safeSerialize(mapper, instance, prefix); + Map properties = sanitize(prefix, serialized, showUnsanitized); + Map inputs = getInputs(prefix, serialized, showUnsanitized); + return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); + }); + } + + private ConfigurationPropertiesBeanDescriptor describeTargetBean(Object bean, + Function 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; } /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java index 5e09952a801..7087262a5a9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java @@ -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 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 { From 3762d39d1b05b432803d3b8ba6148d71a2582b2b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 12 May 2026 13:51:14 -0700 Subject: [PATCH 2/2] Polish "Unwrap AOP proxies in configprops endpoint serialization" See gh-50273 --- ...ConfigurationPropertiesReportEndpoint.java | 74 +++++++------------ ...rtiesReportEndpointSerializationTests.java | 6 +- 2 files changed, 29 insertions(+), 51 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index caaaa8ce49f..e0966be4a74 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -25,7 +25,6 @@ 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 com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -213,63 +212,28 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext private ContextConfigurationPropertiesDescriptor describeBeans(ObjectMapper mapper, ApplicationContext context, Predicate beanFilterPredicate, boolean showUnsanitized) { + ApplicationContext parent = context.getParent(); Map beans = ConfigurationPropertiesBean.getAll(context); Map 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); + beans.values() + .stream() + .filter(beanFilterPredicate) + .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, boolean showUnsanitized) { - return describeTargetBean(bean.getInstance(), (instance) -> { - String prefix = bean.getAnnotation().prefix(); - Map serialized = safeSerialize(mapper, instance, prefix); - Map properties = sanitize(prefix, serialized, showUnsanitized); - Map inputs = getInputs(prefix, serialized, showUnsanitized); - return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); - }); - } - - private ConfigurationPropertiesBeanDescriptor describeTargetBean(Object bean, - Function 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; + String prefix = bean.getAnnotation().prefix(); + Map serialized = safeSerialize(mapper, bean.getInstance(), prefix); + Map properties = sanitize(prefix, serialized, showUnsanitized); + Map inputs = getInputs(prefix, serialized, showUnsanitized); + return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); } /** - * 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 @@ -278,6 +242,18 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext @SuppressWarnings({ "unchecked" }) private Map 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) { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java index 7087262a5a9..72e0c99ed95 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java @@ -293,14 +293,16 @@ class ConfigurationPropertiesReportEndpointSerializationTests { } @Test - void aopProxyWithUnresolvableTargetIsExcluded() { + 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()).doesNotContainKey("foo"); + assertThat(getContextDescriptor(context, applicationProperties).getBeans()).containsKey("foo") + .satisfies((beans) -> assertThat(beans.get("foo").getProperties()).containsEntry("error", + "Cannot serialize 'foo'")); }); }