diff --git a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index 7c6382407de..7de31c52698 100644 --- a/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -23,10 +23,11 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +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; @@ -140,14 +141,14 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext private ContextConfigurationPropertiesDescriptor describeBeans(ApplicationContext context, Predicate beanFilterPredicate, boolean showUnsanitized) { + ApplicationContext parent = context.getParent(); Map beans = ConfigurationPropertiesBean.getAll(context); - Map descriptors = beans.values() + Map descriptors = new LinkedHashMap<>(); + beans.values() .stream() .filter(beanFilterPredicate) - .collect(Collectors.toMap(ConfigurationPropertiesBean::getName, - (bean) -> describeBean(bean, showUnsanitized))); - return new ContextConfigurationPropertiesDescriptor(descriptors, - (context.getParent() != null) ? context.getParent().getId() : null); + .forEach((bean) -> descriptors.put(bean.getName(), describeBean(bean, showUnsanitized))); + return new ContextConfigurationPropertiesDescriptor(descriptors, (parent != null) ? parent.getId() : null); } private ConfigurationPropertiesBeanDescriptor describeBean(ConfigurationPropertiesBean bean, @@ -168,6 +169,18 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext */ private Map safeSerialize(@Nullable Object bean, String prefix) { try { + if (bean instanceof Advised advised) { + TargetSource targetSource = advised.getTargetSource(); + Object target = targetSource.getTarget(); + try { + return safeSerialize(target, prefix); + } + finally { + if (target != null) { + targetSource.releaseTarget(target); + } + } + } return new HashMap<>(this.serializer.serialize(bean)); } catch (Exception ex) { diff --git a/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java b/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java index 355f0a1d8c4..09e67088946 100644 --- a/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java +++ b/module/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java @@ -30,6 +30,8 @@ import com.zaxxer.hikari.HikariDataSource; import org.jspecify.annotations.Nullable; 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; @@ -246,6 +248,58 @@ 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 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) -> { + ConfigurationPropertiesBeanDescriptor bean = beans.get("foo"); + assertThat(bean).isNotNull(); + assertThat(bean.getProperties()).containsEntry("error", "Cannot serialize 'foo'"); + }); + }); + } + @Test void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() { ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -578,6 +632,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 {