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..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 @@ -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 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(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 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 5e09952a801..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 @@ -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 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 {