Skip to content

Commit 6b37d87

Browse files
committed
Allow multiple endpoint PathMapper beans
Update `WebEndpointDiscoverer` and related classes to that multiple `PathMapper` beans can be registered. Mappers are now tried in order until one returns a non-null value. Closes gh-14841
1 parent bebfa76 commit 6b37d87

24 files changed

+164
-81
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
1818

1919
import java.util.Collection;
20+
import java.util.List;
2021

2122
import org.springframework.boot.actuate.endpoint.EndpointFilter;
2223
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
@@ -45,17 +46,17 @@ public class CloudFoundryWebEndpointDiscoverer extends WebEndpointDiscoverer {
4546
* @param applicationContext the source application context
4647
* @param parameterValueMapper the parameter value mapper
4748
* @param endpointMediaTypes the endpoint media types
48-
* @param endpointPathMapper the endpoint path mapper
49+
* @param endpointPathMappers the endpoint path mappers
4950
* @param invokerAdvisors invoker advisors to apply
5051
* @param filters filters to apply
5152
*/
5253
public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext,
5354
ParameterValueMapper parameterValueMapper,
54-
EndpointMediaTypes endpointMediaTypes, PathMapper endpointPathMapper,
55+
EndpointMediaTypes endpointMediaTypes, List<PathMapper> endpointPathMappers,
5556
Collection<OperationInvokerAdvisor> invokerAdvisors,
5657
Collection<EndpointFilter<ExposableWebEndpoint>> filters) {
5758
super(applicationContext, parameterValueMapper, endpointMediaTypes,
58-
endpointPathMapper, invokerAdvisors, filters);
59+
endpointPathMappers, invokerAdvisors, filters);
5960
}
6061

6162
@Override

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3434
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
3535
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
36-
import org.springframework.boot.actuate.endpoint.web.PathMapper;
3736
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
3837
import org.springframework.boot.actuate.health.HealthEndpoint;
3938
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
@@ -95,9 +94,8 @@ public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHand
9594
WebClient.Builder webClientBuilder,
9695
ControllerEndpointsSupplier controllerEndpointsSupplier) {
9796
CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer(
98-
this.applicationContext, parameterMapper, endpointMediaTypes,
99-
PathMapper.useEndpointId(), Collections.emptyList(),
100-
Collections.emptyList());
97+
this.applicationContext, parameterMapper, endpointMediaTypes, null,
98+
Collections.emptyList(), Collections.emptyList());
10199
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
102100
webClientBuilder, this.applicationContext.getEnvironment());
103101
Collection<ExposableWebEndpoint> webEndpoints = endpointDiscoverer.getEndpoints();

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3333
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
3434
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
35-
import org.springframework.boot.actuate.endpoint.web.PathMapper;
3635
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
3736
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
3837
import org.springframework.boot.actuate.health.HealthEndpoint;
@@ -99,9 +98,8 @@ public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServl
9998
ServletEndpointsSupplier servletEndpointsSupplier,
10099
ControllerEndpointsSupplier controllerEndpointsSupplier) {
101100
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(
102-
this.applicationContext, parameterMapper, endpointMediaTypes,
103-
PathMapper.useEndpointId(), Collections.emptyList(),
104-
Collections.emptyList());
101+
this.applicationContext, parameterMapper, endpointMediaTypes, null,
102+
Collections.emptyList(), Collections.emptyList());
105103
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
106104
restTemplateBuilder, this.applicationContext.getEnvironment());
107105
Collection<ExposableWebEndpoint> webEndpoints = discoverer.getEndpoints();

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/MappingWebEndpointPathMapper.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import org.springframework.boot.actuate.endpoint.EndpointId;
2323
import org.springframework.boot.actuate.endpoint.web.PathMapper;
24+
import org.springframework.core.Ordered;
25+
import org.springframework.core.annotation.Order;
2426
import org.springframework.util.StringUtils;
2527

2628
/**
@@ -29,6 +31,7 @@
2931
*
3032
* @author Stephane Nicoll
3133
*/
34+
@Order(Ordered.HIGHEST_PRECEDENCE)
3235
class MappingWebEndpointPathMapper implements PathMapper {
3336

3437
private final Map<EndpointId, String> pathMapping;
@@ -42,8 +45,7 @@ class MappingWebEndpointPathMapper implements PathMapper {
4245
@Override
4346
public String getRootPath(EndpointId endpointId) {
4447
String path = this.pathMapping.get(endpointId);
45-
return StringUtils.hasText(path) ? path
46-
: PathMapper.useEndpointId().getRootPath(endpointId);
48+
return StringUtils.hasText(path) ? path : null;
4749
}
4850

4951
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public WebEndpointAutoConfiguration(ApplicationContext applicationContext,
8080
}
8181

8282
@Bean
83-
@ConditionalOnMissingBean
8483
public PathMapper webEndpointPathMapper() {
8584
return new MappingWebEndpointPathMapper(this.properties.getPathMapping());
8685
}
@@ -95,22 +94,25 @@ public EndpointMediaTypes endpointMediaTypes() {
9594
@ConditionalOnMissingBean(WebEndpointsSupplier.class)
9695
public WebEndpointDiscoverer webEndpointDiscoverer(
9796
ParameterValueMapper parameterValueMapper,
98-
EndpointMediaTypes endpointMediaTypes, PathMapper webEndpointPathMapper,
97+
EndpointMediaTypes endpointMediaTypes,
98+
ObjectProvider<PathMapper> endpointPathMappers,
9999
ObjectProvider<OperationInvokerAdvisor> invokerAdvisors,
100100
ObjectProvider<EndpointFilter<ExposableWebEndpoint>> filters) {
101101
return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper,
102-
endpointMediaTypes, webEndpointPathMapper,
102+
endpointMediaTypes,
103+
endpointPathMappers.orderedStream().collect(Collectors.toList()),
103104
invokerAdvisors.orderedStream().collect(Collectors.toList()),
104105
filters.orderedStream().collect(Collectors.toList()));
105106
}
106107

107108
@Bean
108109
@ConditionalOnMissingBean(ControllerEndpointsSupplier.class)
109110
public ControllerEndpointDiscoverer controllerEndpointDiscoverer(
110-
PathMapper webEndpointPathMapper,
111+
ObjectProvider<PathMapper> endpointPathMappers,
111112
ObjectProvider<Collection<EndpointFilter<ExposableControllerEndpoint>>> filters) {
112113
return new ControllerEndpointDiscoverer(this.applicationContext,
113-
webEndpointPathMapper, filters.getIfAvailable(Collections::emptyList));
114+
endpointPathMappers.orderedStream().collect(Collectors.toList()),
115+
filters.getIfAvailable(Collections::emptyList));
114116
}
115117

116118
@Bean
@@ -144,10 +146,11 @@ static class WebEndpointServletConfiguration {
144146
@Bean
145147
@ConditionalOnMissingBean(ServletEndpointsSupplier.class)
146148
public ServletEndpointDiscoverer servletEndpointDiscoverer(
147-
ApplicationContext applicationContext, PathMapper webEndpointPathMapper,
149+
ApplicationContext applicationContext,
150+
ObjectProvider<PathMapper> endpointPathMappers,
148151
ObjectProvider<EndpointFilter<ExposableServletEndpoint>> filters) {
149152
return new ServletEndpointDiscoverer(applicationContext,
150-
webEndpointPathMapper,
153+
endpointPathMappers.orderedStream().collect(Collectors.toList()),
151154
filters.orderedStream().collect(Collectors.toList()));
152155
}
153156

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ private void load(Function<EndpointId, Long> timeToLive,
9595
Collections.singletonList("application/json"),
9696
Collections.singletonList("application/json"));
9797
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(
98-
context, parameterMapper, mediaTypes, endpointPathMapper,
98+
context, parameterMapper, mediaTypes,
99+
Collections.singletonList(endpointPathMapper),
99100
Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)),
100101
Collections.emptyList());
101102
consumer.accept(discoverer);

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3737
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3838
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
39-
import org.springframework.boot.actuate.endpoint.web.PathMapper;
4039
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
4140
import org.springframework.boot.autoconfigure.AutoConfigurations;
4241
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
@@ -220,8 +219,8 @@ public WebEndpointDiscoverer webEndpointDiscoverer(
220219
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
221220
DefaultConversionService.getSharedInstance());
222221
return new WebEndpointDiscoverer(applicationContext, parameterMapper,
223-
endpointMediaTypes, PathMapper.useEndpointId(),
224-
Collections.emptyList(), Collections.emptyList());
222+
endpointMediaTypes, null, Collections.emptyList(),
223+
Collections.emptyList());
225224
}
226225

227226
@Bean

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3737
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3838
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
39-
import org.springframework.boot.actuate.endpoint.web.PathMapper;
4039
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
4140
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
4241
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
@@ -219,8 +218,8 @@ public WebEndpointDiscoverer webEndpointDiscoverer(
219218
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
220219
DefaultConversionService.getSharedInstance());
221220
return new WebEndpointDiscoverer(applicationContext, parameterMapper,
222-
endpointMediaTypes, PathMapper.useEndpointId(),
223-
Collections.emptyList(), Collections.emptyList());
221+
endpointMediaTypes, null, Collections.emptyList(),
222+
Collections.emptyList());
224223
}
225224

226225
@Bean

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/MappingWebEndpointPathMapperTests.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.junit.Test;
2222

2323
import org.springframework.boot.actuate.endpoint.EndpointId;
24+
import org.springframework.boot.actuate.endpoint.web.PathMapper;
2425

2526
import static org.assertj.core.api.Assertions.assertThat;
2627

@@ -35,29 +36,32 @@ public class MappingWebEndpointPathMapperTests {
3536
public void defaultConfiguration() {
3637
MappingWebEndpointPathMapper mapper = new MappingWebEndpointPathMapper(
3738
Collections.emptyMap());
38-
assertThat(mapper.getRootPath(EndpointId.of("test"))).isEqualTo("test");
39+
assertThat(PathMapper.getRootPath(Collections.singletonList(mapper),
40+
EndpointId.of("test"))).isEqualTo("test");
3941
}
4042

4143
@Test
4244
public void userConfiguration() {
4345
MappingWebEndpointPathMapper mapper = new MappingWebEndpointPathMapper(
4446
Collections.singletonMap("test", "custom"));
45-
assertThat(mapper.getRootPath(EndpointId.of("test"))).isEqualTo("custom");
47+
assertThat(PathMapper.getRootPath(Collections.singletonList(mapper),
48+
EndpointId.of("test"))).isEqualTo("custom");
4649
}
4750

4851
@Test
4952
public void mixedCaseDefaultConfiguration() {
5053
MappingWebEndpointPathMapper mapper = new MappingWebEndpointPathMapper(
5154
Collections.emptyMap());
52-
assertThat(mapper.getRootPath(EndpointId.of("testEndpoint")))
53-
.isEqualTo("testEndpoint");
55+
assertThat(PathMapper.getRootPath(Collections.singletonList(mapper),
56+
EndpointId.of("testEndpoint"))).isEqualTo("testEndpoint");
5457
}
5558

5659
@Test
5760
public void mixedCaseUserConfiguration() {
5861
MappingWebEndpointPathMapper mapper = new MappingWebEndpointPathMapper(
5962
Collections.singletonMap("test-endpoint", "custom"));
60-
assertThat(mapper.getRootPath(EndpointId.of("testEndpoint"))).isEqualTo("custom");
63+
assertThat(PathMapper.getRootPath(Collections.singletonList(mapper),
64+
EndpointId.of("testEndpoint"))).isEqualTo("custom");
6165
}
6266

6367
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,29 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.endpoint.web;
1818

19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
1924
import org.junit.Test;
2025

2126
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
2227
import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter;
2328
import org.springframework.boot.actuate.endpoint.EndpointId;
29+
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
2430
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
2531
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
32+
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
33+
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
2634
import org.springframework.boot.actuate.endpoint.web.PathMapper;
2735
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer;
2836
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointDiscoverer;
2937
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
3038
import org.springframework.boot.autoconfigure.AutoConfigurations;
3139
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3240
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
41+
import org.springframework.stereotype.Component;
3342

3443
import static org.assertj.core.api.Assertions.assertThat;
3544

@@ -71,6 +80,27 @@ public void webApplicationConfiguresPathMapper() {
7180
});
7281
}
7382

83+
@Test
84+
public void webApplicationSupportCustomPathMatcher() {
85+
this.contextRunner
86+
.withPropertyValues("management.endpoints.web.exposure.include=*",
87+
"management.endpoints.web.path-mapping.testanotherone=foo")
88+
.withUserConfiguration(TestPathMatcher.class, TestOneEndpoint.class,
89+
TestAnotherOneEndpoint.class, TestTwoEndpoint.class)
90+
.run((context) -> {
91+
WebEndpointDiscoverer discoverer = context
92+
.getBean(WebEndpointDiscoverer.class);
93+
Collection<ExposableWebEndpoint> endpoints = discoverer
94+
.getEndpoints();
95+
ExposableWebEndpoint[] webEndpoints = endpoints
96+
.toArray(new ExposableWebEndpoint[0]);
97+
List<String> paths = Arrays.stream(webEndpoints)
98+
.map(PathMappedEndpoint::getRootPath)
99+
.collect(Collectors.toList());
100+
assertThat(paths).containsOnly("1/testone", "foo", "testtwo");
101+
});
102+
}
103+
74104
@Test
75105
public void webApplicationConfiguresEndpointDiscoverer() {
76106
this.contextRunner.run((context) -> {
@@ -100,4 +130,35 @@ public void contextWhenNotServletShouldNotConfigureServletEndpointDiscoverer() {
100130
.doesNotHaveBean(ServletEndpointDiscoverer.class));
101131
}
102132

133+
@Component
134+
private static class TestPathMatcher implements PathMapper {
135+
136+
@Override
137+
public String getRootPath(EndpointId endpointId) {
138+
if (endpointId.toString().endsWith("one")) {
139+
return "1/" + endpointId.toString();
140+
}
141+
return null;
142+
}
143+
144+
}
145+
146+
@Component
147+
@Endpoint(id = "testone")
148+
private static class TestOneEndpoint {
149+
150+
}
151+
152+
@Component
153+
@Endpoint(id = "testanotherone")
154+
private static class TestAnotherOneEndpoint {
155+
156+
}
157+
158+
@Component
159+
@Endpoint(id = "testtwo")
160+
private static class TestTwoEndpoint {
161+
162+
}
163+
103164
}

0 commit comments

Comments
 (0)