Skip to content

Commit c66ea41

Browse files
committed
Merge pull request #50273 from zxuhan
Closes gh-50273 * pr/50273: Polish "Unwrap AOP proxies in configprops endpoint serialization" Unwrap AOP proxies in configprops endpoint serialization
2 parents 328c65d + 3762d39 commit c66ea41

2 files changed

Lines changed: 144 additions & 8 deletions

File tree

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.List;
2727
import java.util.Map;
2828
import java.util.function.Predicate;
29-
import java.util.stream.Collectors;
3029

3130
import com.fasterxml.jackson.annotation.JsonInclude.Include;
3231
import com.fasterxml.jackson.core.JsonGenerator;
@@ -53,6 +52,8 @@
5352
import org.apache.commons.logging.Log;
5453
import org.apache.commons.logging.LogFactory;
5554

55+
import org.springframework.aop.TargetSource;
56+
import org.springframework.aop.framework.Advised;
5657
import org.springframework.beans.BeansException;
5758
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
5859
import org.springframework.boot.actuate.endpoint.SanitizableData;
@@ -211,14 +212,14 @@ private void applySerializationModifier(JsonMapper.Builder builder) {
211212

212213
private ContextConfigurationPropertiesDescriptor describeBeans(ObjectMapper mapper, ApplicationContext context,
213214
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
215+
ApplicationContext parent = context.getParent();
214216
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
215-
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values()
217+
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new LinkedHashMap<>();
218+
beans.values()
216219
.stream()
217220
.filter(beanFilterPredicate)
218-
.collect(Collectors.toMap(ConfigurationPropertiesBean::getName,
219-
(bean) -> describeBean(mapper, bean, showUnsanitized)));
220-
return new ContextConfigurationPropertiesDescriptor(descriptors,
221-
(context.getParent() != null) ? context.getParent().getId() : null);
221+
.forEach((bean) -> descriptors.put(bean.getName(), describeBean(mapper, bean, showUnsanitized)));
222+
return new ContextConfigurationPropertiesDescriptor(descriptors, (parent != null) ? parent.getId() : null);
222223
}
223224

224225
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean,
@@ -231,8 +232,8 @@ private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper,
231232
}
232233

233234
/**
234-
* Cautiously serialize the bean to a map (returning a map with an error message
235-
* instead of throwing an exception if there is a problem).
235+
* Cautiously serialize the ultimate bean target to a map (returning a map with an
236+
* error message instead of throwing an exception if there is a problem).
236237
* @param mapper the object mapper
237238
* @param bean the source bean
238239
* @param prefix the prefix
@@ -241,6 +242,18 @@ private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper,
241242
@SuppressWarnings({ "unchecked" })
242243
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, String prefix) {
243244
try {
245+
if (bean instanceof Advised advised) {
246+
TargetSource targetSource = advised.getTargetSource();
247+
Object target = targetSource.getTarget();
248+
try {
249+
return safeSerialize(mapper, target, prefix);
250+
}
251+
finally {
252+
if (target != null) {
253+
targetSource.releaseTarget(target);
254+
}
255+
}
256+
}
244257
return new HashMap<>(mapper.convertValue(bean, Map.class));
245258
}
246259
catch (Exception ex) {

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@
2929
import com.zaxxer.hikari.HikariDataSource;
3030
import org.junit.jupiter.api.Test;
3131

32+
import org.springframework.aop.TargetSource;
33+
import org.springframework.aop.framework.ProxyFactory;
3234
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
3335
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesDescriptor;
36+
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationPropertiesDescriptor;
3437
import org.springframework.boot.actuate.endpoint.Show;
3538
import org.springframework.boot.context.properties.ConfigurationProperties;
3639
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
3740
import org.springframework.boot.context.properties.EnableConfigurationProperties;
41+
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
3842
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3943
import org.springframework.context.annotation.Bean;
4044
import org.springframework.context.annotation.Configuration;
@@ -253,6 +257,55 @@ void testInitializedMapAndList() {
253257
});
254258
}
255259

260+
@Test
261+
void aopProxyDoesNotLeakAdvisedInternals() {
262+
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
263+
.withUserConfiguration(CglibProxiedFooConfig.class)
264+
.withPropertyValues("foo.name:test");
265+
contextRunner.run((context) -> {
266+
ConfigurationPropertiesReportEndpoint endpoint = context
267+
.getBean(ConfigurationPropertiesReportEndpoint.class);
268+
ConfigurationPropertiesDescriptor applicationProperties = endpoint.configurationProperties();
269+
ConfigurationPropertiesBeanDescriptor foo = getContextDescriptor(context, applicationProperties).getBeans()
270+
.get("foo");
271+
assertThat(foo).isNotNull();
272+
Map<String, Object> map = foo.getProperties();
273+
assertThat(map).containsOnlyKeys("name", "bar");
274+
assertThat(map).containsEntry("name", "test");
275+
});
276+
}
277+
278+
@Test
279+
void aopProxyTargetingAnotherProxyIsUnwrapped() {
280+
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
281+
.withUserConfiguration(NestedCglibProxiedFooConfig.class)
282+
.withPropertyValues("foo.name:nested");
283+
contextRunner.run((context) -> {
284+
ConfigurationPropertiesReportEndpoint endpoint = context
285+
.getBean(ConfigurationPropertiesReportEndpoint.class);
286+
ConfigurationPropertiesDescriptor applicationProperties = endpoint.configurationProperties();
287+
ConfigurationPropertiesBeanDescriptor foo = getContextDescriptor(context, applicationProperties).getBeans()
288+
.get("foo");
289+
assertThat(foo).isNotNull();
290+
assertThat(foo.getProperties()).containsOnlyKeys("name", "bar");
291+
assertThat(foo.getProperties()).containsEntry("name", "nested");
292+
});
293+
}
294+
295+
@Test
296+
void aopProxyWithUnresolvableTarget() {
297+
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
298+
.withUserConfiguration(UnresolvableTargetFooConfig.class);
299+
contextRunner.run((context) -> {
300+
ConfigurationPropertiesReportEndpoint endpoint = context
301+
.getBean(ConfigurationPropertiesReportEndpoint.class);
302+
ConfigurationPropertiesDescriptor applicationProperties = endpoint.configurationProperties();
303+
assertThat(getContextDescriptor(context, applicationProperties).getBeans()).containsKey("foo")
304+
.satisfies((beans) -> assertThat(beans.get("foo").getProperties()).containsEntry("error",
305+
"Cannot serialize 'foo'"));
306+
});
307+
}
308+
256309
@Test
257310
void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() {
258311
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -313,6 +366,14 @@ void endpointResponseUsesPlaceholderForComplexValueAsPropertyValue() {
313366
});
314367
}
315368

369+
private ContextConfigurationPropertiesDescriptor getContextDescriptor(AssertableApplicationContext context,
370+
ConfigurationPropertiesDescriptor applicationProperties) {
371+
ContextConfigurationPropertiesDescriptor contextDescriptor = applicationProperties.getContexts()
372+
.get(context.getId());
373+
assertThat(contextDescriptor).isNotNull();
374+
return contextDescriptor;
375+
}
376+
316377
@Configuration(proxyBeanMethods = false)
317378
@EnableConfigurationProperties
318379
static class Base {
@@ -576,6 +637,68 @@ Cycle cycle() {
576637

577638
}
578639

640+
@Configuration(proxyBeanMethods = false)
641+
@Import(Base.class)
642+
static class CglibProxiedFooConfig {
643+
644+
@Bean
645+
@ConfigurationProperties("foo")
646+
Foo foo() {
647+
ProxyFactory proxyFactory = new ProxyFactory(new Foo());
648+
proxyFactory.setProxyTargetClass(true);
649+
return (Foo) proxyFactory.getProxy();
650+
}
651+
652+
}
653+
654+
@Configuration(proxyBeanMethods = false)
655+
@Import(Base.class)
656+
static class NestedCglibProxiedFooConfig {
657+
658+
@Bean
659+
@ConfigurationProperties("foo")
660+
Foo foo() {
661+
ProxyFactory inner = new ProxyFactory(new Foo());
662+
inner.setProxyTargetClass(true);
663+
ProxyFactory outer = new ProxyFactory(inner.getProxy());
664+
outer.setProxyTargetClass(true);
665+
return (Foo) outer.getProxy();
666+
}
667+
668+
}
669+
670+
@Configuration(proxyBeanMethods = false)
671+
@Import(Base.class)
672+
static class UnresolvableTargetFooConfig {
673+
674+
@Bean
675+
@ConfigurationProperties("foo")
676+
Foo foo() {
677+
ProxyFactory proxyFactory = new ProxyFactory();
678+
proxyFactory.setProxyTargetClass(true);
679+
proxyFactory.setTargetSource(new TargetSource() {
680+
681+
@Override
682+
public Class<?> getTargetClass() {
683+
return Foo.class;
684+
}
685+
686+
@Override
687+
public boolean isStatic() {
688+
return false;
689+
}
690+
691+
@Override
692+
public Object getTarget() throws Exception {
693+
throw new IllegalStateException("no target");
694+
}
695+
696+
});
697+
return (Foo) proxyFactory.getProxy();
698+
}
699+
700+
}
701+
579702
@Configuration(proxyBeanMethods = false)
580703
@EnableConfigurationProperties
581704
static class HikariDataSourceConfig {

0 commit comments

Comments
 (0)