Merge branch '3.5.x' into 4.0.x

Closes gh-50416
This commit is contained in:
Phillip Webb
2026-05-12 18:10:38 -07:00
2 changed files with 135 additions and 6 deletions
@@ -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<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(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<String, @Nullable Object> 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) {
@@ -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<String, @Nullable 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) -> {
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 {