Skip to content

Commit 56afc25

Browse files
committed
Allow to customize the path of a web endpoint
This commit introduces a endpoints.<id>.web.path generic property that allows to customize the path of an endpoint. By default the path is the same as the id of the endpoint. Such customization does not apply for the CloudFoundry specific endpoints. Closes gh-10181
1 parent 622e65a commit 56afc25

File tree

16 files changed

+244
-26
lines changed

16 files changed

+244
-26
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServl
8282
RestTemplateBuilder builder) {
8383
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
8484
this.applicationContext, parameterMapper, cachingConfigurationFactory,
85-
endpointMediaTypes);
85+
endpointMediaTypes, (id) -> id);
8686
return new CloudFoundryWebEndpointServletHandlerMapping(
8787
new EndpointMapping("/cloudfoundryapplication"),
8888
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.endpoint;
18+
19+
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
20+
import org.springframework.core.env.Environment;
21+
22+
/**
23+
* Default {@link EndpointPathResolver} implementation that use the
24+
* {@link Environment} to determine if an endpoint has a custom path.
25+
*
26+
* @author Stephane Nicoll
27+
*/
28+
class DefaultEndpointPathResolver implements EndpointPathResolver {
29+
30+
private final Environment environment;
31+
32+
DefaultEndpointPathResolver(Environment environment) {
33+
this.environment = environment;
34+
}
35+
36+
@Override
37+
public String resolvePath(String endpointId) {
38+
String key = String.format("endpoints.%s.web.path", endpointId);
39+
return this.environment.getProperty(key, String.class, endpointId);
40+
}
41+
42+
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper;
2727
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
2828
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
29+
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
2930
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
3031
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
3132
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -77,14 +78,22 @@ public EndpointMediaTypes endpointMediaTypes() {
7778
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES);
7879
}
7980

81+
@Bean
82+
@ConditionalOnMissingBean
83+
public EndpointPathResolver endpointPathResolver(
84+
Environment environment) {
85+
return new DefaultEndpointPathResolver(environment);
86+
}
87+
8088
@Bean
8189
public EndpointProvider<WebEndpointOperation> webEndpointProvider(
8290
OperationParameterMapper parameterMapper,
83-
DefaultCachingConfigurationFactory cachingConfigurationFactory) {
91+
DefaultCachingConfigurationFactory cachingConfigurationFactory,
92+
EndpointPathResolver endpointPathResolver) {
8493
Environment environment = this.applicationContext.getEnvironment();
8594
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
8695
this.applicationContext, parameterMapper, cachingConfigurationFactory,
87-
endpointMediaTypes());
96+
endpointMediaTypes(), endpointPathResolver);
8897
return new EndpointProvider<>(environment, endpointDiscoverer,
8998
EndpointExposure.WEB);
9099
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,22 @@ public void allEndpointsAvailableUnderCloudFoundryWithoutEnablingWeb()
219219
assertThat(endpoints.get(0).getId()).isEqualTo("test");
220220
}
221221

222+
@Test
223+
public void endpointPathCustomizationIsNotApplied()
224+
throws Exception {
225+
TestPropertyValues.of("endpoints.test.web.path=another/custom")
226+
.applyTo(this.context);
227+
this.context.register(TestConfiguration.class);
228+
this.context.refresh();
229+
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping();
230+
List<EndpointInfo<WebEndpointOperation>> endpoints = (List<EndpointInfo<WebEndpointOperation>>) handlerMapping
231+
.getEndpoints();
232+
assertThat(endpoints.size()).isEqualTo(1);
233+
assertThat(endpoints.get(0).getOperations()).hasSize(1);
234+
assertThat(endpoints.get(0).getOperations().iterator().next()
235+
.getRequestPredicate().getPath()).isEqualTo("test");
236+
}
237+
222238
private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping() {
223239
TestPropertyValues
224240
.of("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public WebAnnotationEndpointDiscoverer webEndpointDiscoverer(
219219
DefaultConversionService.getSharedInstance());
220220
return new WebAnnotationEndpointDiscoverer(applicationContext,
221221
parameterMapper, (id) -> new CachingConfiguration(0),
222-
endpointMediaTypes);
222+
endpointMediaTypes, (id) -> id);
223223
}
224224

225225
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.endpoint;
18+
19+
import org.junit.Test;
20+
21+
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
22+
import org.springframework.mock.env.MockEnvironment;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
/**
27+
* Tests for {@link DefaultEndpointPathResolver}.
28+
*
29+
* @author Stephane Nicoll
30+
*/
31+
public class DefaultEndpointPathResolverTests {
32+
33+
private final MockEnvironment environment = new MockEnvironment();
34+
35+
private final EndpointPathResolver resolver = new DefaultEndpointPathResolver(
36+
this.environment);
37+
38+
@Test
39+
public void defaultConfiguration() {
40+
assertThat(this.resolver.resolvePath("test")).isEqualTo("test");
41+
}
42+
43+
@Test
44+
public void userConfiguration() {
45+
this.environment.setProperty("endpoints.test.web.path", "custom");
46+
assertThat(this.resolver.resolvePath("test")).isEqualTo("custom");
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.endpoint.web;
18+
19+
/**
20+
* Resolve the path of an endpoint.
21+
*
22+
* @author Stephane Nicoll
23+
* @since 2.0.0
24+
*/
25+
@FunctionalInterface
26+
public interface EndpointPathResolver {
27+
28+
/**
29+
* Resolve the path for the endpoint with the specified {@code endpointId}.
30+
* @param endpointId the id of an endpoint
31+
* @return the path of the endpoint
32+
*/
33+
String resolvePath(String endpointId);
34+
35+
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebAnnotationEndpointDiscoverer.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory;
4141
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker;
4242
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
43+
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
4344
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
4445
import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod;
4546
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
@@ -71,14 +72,17 @@ public class WebAnnotationEndpointDiscoverer extends
7172
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
7273
* @param endpointMediaTypes the media types produced and consumed by web endpoint
7374
* operations
75+
* @param endpointPathResolver the {@link EndpointPathResolver} used to resolve
76+
* endpoint paths
7477
*/
7578
public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
7679
OperationParameterMapper operationParameterMapper,
7780
CachingConfigurationFactory cachingConfigurationFactory,
78-
EndpointMediaTypes endpointMediaTypes) {
81+
EndpointMediaTypes endpointMediaTypes,
82+
EndpointPathResolver endpointPathResolver) {
7983
super(applicationContext,
8084
new WebEndpointOperationFactory(operationParameterMapper,
81-
endpointMediaTypes),
85+
endpointMediaTypes, endpointPathResolver),
8286
WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
8387
}
8488

@@ -121,10 +125,14 @@ private static final class WebEndpointOperationFactory
121125

122126
private final EndpointMediaTypes endpointMediaTypes;
123127

128+
private final EndpointPathResolver endpointPathResolver;
129+
124130
private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
125-
EndpointMediaTypes endpointMediaTypes) {
131+
EndpointMediaTypes endpointMediaTypes,
132+
EndpointPathResolver endpointPathResolver) {
126133
this.parameterMapper = parameterMapper;
127134
this.endpointMediaTypes = endpointMediaTypes;
135+
this.endpointPathResolver = endpointPathResolver;
128136
}
129137

130138
@Override
@@ -147,7 +155,8 @@ public WebEndpointOperation createOperation(String endpointId,
147155
}
148156

149157
private String determinePath(String endpointId, Method operationMethod) {
150-
StringBuilder path = new StringBuilder(endpointId);
158+
StringBuilder path = new StringBuilder(
159+
this.endpointPathResolver.resolvePath(endpointId));
151160
Stream.of(operationMethod.getParameters())
152161
.filter((
153162
parameter) -> parameter.getAnnotation(Selector.class) != null)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ public WebAnnotationEndpointDiscoverer webEndpointDiscoverer(
385385
DefaultConversionService.getSharedInstance());
386386
return new WebAnnotationEndpointDiscoverer(applicationContext,
387387
parameterMapper, (id) -> new CachingConfiguration(0),
388-
endpointMediaTypes());
388+
endpointMediaTypes(), (id) -> id);
389389
}
390390

391391
@Bean

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ public void twoOperationsOnSameEndpointClashWhenSelectorsHaveDifferentNames() {
189189

190190
@Test
191191
public void endpointMainReadOperationIsCachedWithMatchingId() {
192-
load((id) -> new CachingConfiguration(500), TestEndpointConfiguration.class,
193-
(discoverer) -> {
192+
load((id) -> new CachingConfiguration(500), (id) -> id,
193+
TestEndpointConfiguration.class, (discoverer) -> {
194194
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
195195
discoverer.discoverEndpoints());
196196
assertThat(endpoints).containsOnlyKeys("test");
@@ -237,12 +237,29 @@ public void operationCanProduceCustomMediaTypes() {
237237
});
238238
}
239239

240+
@Test
241+
public void endpointPathCanBeCustomized() {
242+
load((id) -> null, (id) -> "custom/" + id,
243+
AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> {
244+
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
245+
discoverer.discoverEndpoints());
246+
assertThat(endpoints).containsOnlyKeys("test");
247+
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("test");
248+
assertThat(requestPredicates(endpoint)).has(requestPredicates(
249+
path("custom/test").httpMethod(WebEndpointHttpMethod.GET).consumes()
250+
.produces("application/json"),
251+
path("custom/test/{id}").httpMethod(WebEndpointHttpMethod.GET).consumes()
252+
.produces("application/json")));
253+
});
254+
}
255+
240256
private void load(Class<?> configuration,
241257
Consumer<WebAnnotationEndpointDiscoverer> consumer) {
242-
this.load((id) -> null, configuration, consumer);
258+
this.load((id) -> null, (id) -> id, configuration, consumer);
243259
}
244260

245261
private void load(CachingConfigurationFactory cachingConfigurationFactory,
262+
EndpointPathResolver endpointPathResolver,
246263
Class<?> configuration, Consumer<WebAnnotationEndpointDiscoverer> consumer) {
247264
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
248265
configuration);
@@ -254,7 +271,8 @@ private void load(CachingConfigurationFactory cachingConfigurationFactory,
254271
cachingConfigurationFactory,
255272
new EndpointMediaTypes(
256273
Collections.singletonList("application/json"),
257-
Collections.singletonList("application/json"))));
274+
Collections.singletonList("application/json")),
275+
endpointPathResolver));
258276
}
259277
finally {
260278
context.close();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ private void customize(ResourceConfig config) {
9999
WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer(
100100
this.applicationContext,
101101
new ConversionServiceOperationParameterMapper(), (id) -> null,
102-
endpointMediaTypes);
102+
endpointMediaTypes, (id) -> id);
103103
Collection<Resource> resources = new JerseyEndpointResourceFactory()
104104
.createEndpointResources(new EndpointMapping("/application"),
105105
discoverer.discoverEndpoints(), endpointMediaTypes);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() {
105105
WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer(
106106
this.applicationContext,
107107
new ConversionServiceOperationParameterMapper(), (id) -> null,
108-
endpointMediaTypes);
108+
endpointMediaTypes, (id) -> id);
109109
return new WebFluxEndpointHandlerMapping(new EndpointMapping("/application"),
110110
discoverer.discoverEndpoints(), endpointMediaTypes,
111111
new CorsConfiguration());

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() {
8888
WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer(
8989
this.applicationContext,
9090
new ConversionServiceOperationParameterMapper(), (id) -> null,
91-
endpointMediaTypes);
91+
endpointMediaTypes, (id) -> id);
9292
return new WebMvcEndpointHandlerMapping(new EndpointMapping("/application"),
9393
discoverer.discoverEndpoints(), endpointMediaTypes,
9494
new CorsConfiguration());

0 commit comments

Comments
 (0)