Skip to content

Commit a747bcf

Browse files
Puppy4Cwilkinsona
authored andcommitted
Add support for MVC router functions to mappings endpoint
See gh-44163 Signed-off-by: puppy4c <[email protected]>
1 parent 4c097b9 commit a747bcf

File tree

5 files changed

+156
-9
lines changed

5 files changed

+156
-9
lines changed

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

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,13 +47,17 @@
4747
import org.springframework.test.web.reactive.server.WebTestClient;
4848
import org.springframework.web.bind.annotation.PostMapping;
4949
import org.springframework.web.bind.annotation.RestController;
50+
import org.springframework.web.servlet.function.RouterFunction;
51+
import org.springframework.web.servlet.function.RouterFunctions;
52+
import org.springframework.web.servlet.function.ServerResponse;
5053

5154
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
5255
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
5356
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
5457
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
5558
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
5659
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
60+
import static org.springframework.web.servlet.function.RequestPredicates.GET;
5761

5862
/**
5963
* Tests for generating documentation describing {@link MappingsEndpoint}.
@@ -130,6 +134,13 @@ void mappings() {
130134
fieldWithPath("*.[].details.handlerMethod.name").description("Name of the method."),
131135
fieldWithPath("*.[].details.handlerMethod.descriptor")
132136
.description("Descriptor of the method as specified in the Java Language Specification."));
137+
List<FieldDescriptor> handlerFunction = List.of(
138+
fieldWithPath("*.[].details.handlerFunction").optional()
139+
.type(JsonFieldType.OBJECT)
140+
.description("Details of the function, if any, that will handle requests to this mapping."),
141+
fieldWithPath("*.[].details.handlerFunction.className").type(JsonFieldType.STRING)
142+
.description("Fully qualified name of the class of the function."));
143+
dispatcherServletFields.addAll(handlerFunction);
133144
dispatcherServletFields.addAll(handlerMethod);
134145
dispatcherServletFields.addAll(requestMappingConditions);
135146
this.client.get()
@@ -192,6 +203,11 @@ ExampleController exampleController() {
192203
return new ExampleController();
193204
}
194205

206+
@Bean
207+
RouterFunction<ServerResponse> exampleRouter() {
208+
return RouterFunctions.route(GET("/foo"), (request) -> ServerResponse.ok().build());
209+
}
210+
195211
}
196212

197213
@RestController

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletMappingDetails.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,8 @@ public class DispatcherServletMappingDetails {
2929

3030
private HandlerMethodDescription handlerMethod;
3131

32+
private HandlerFunctionDescription handlerFunction;
33+
3234
private RequestMappingConditionsDescription requestMappingConditions;
3335

3436
public HandlerMethodDescription getHandlerMethod() {
@@ -39,6 +41,14 @@ void setHandlerMethod(HandlerMethodDescription handlerMethod) {
3941
this.handlerMethod = handlerMethod;
4042
}
4143

44+
public HandlerFunctionDescription getHandlerFunction() {
45+
return this.handlerFunction;
46+
}
47+
48+
void setHandlerFunction(HandlerFunctionDescription handlerFunction) {
49+
this.handlerFunction = handlerFunction;
50+
}
51+
4252
public RequestMappingConditionsDescription getRequestMappingConditions() {
4353
return this.requestMappingConditions;
4454
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java

+67-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@
2323
import java.util.List;
2424
import java.util.Map;
2525
import java.util.Map.Entry;
26+
import java.util.Optional;
27+
import java.util.function.Function;
2628
import java.util.stream.Stream;
2729

2830
import jakarta.servlet.Servlet;
@@ -36,10 +38,17 @@
3638
import org.springframework.boot.web.servlet.ServletRegistrationBean;
3739
import org.springframework.context.ApplicationContext;
3840
import org.springframework.context.annotation.ImportRuntimeHints;
41+
import org.springframework.core.io.Resource;
3942
import org.springframework.web.context.WebApplicationContext;
4043
import org.springframework.web.method.HandlerMethod;
4144
import org.springframework.web.servlet.DispatcherServlet;
4245
import org.springframework.web.servlet.HandlerMapping;
46+
import org.springframework.web.servlet.function.HandlerFunction;
47+
import org.springframework.web.servlet.function.RequestPredicate;
48+
import org.springframework.web.servlet.function.RouterFunction;
49+
import org.springframework.web.servlet.function.RouterFunctions.Visitor;
50+
import org.springframework.web.servlet.function.ServerRequest;
51+
import org.springframework.web.servlet.function.support.RouterFunctionMapping;
4352
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
4453
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
4554
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
@@ -63,6 +72,7 @@ public class DispatcherServletsMappingDescriptionProvider implements MappingDesc
6372
providers.add(new RequestMappingInfoHandlerMappingDescriptionProvider());
6473
providers.add(new UrlHandlerMappingDescriptionProvider());
6574
providers.add(new IterableDelegatesHandlerMappingDescriptionProvider(new ArrayList<>(providers)));
75+
providers.add(new RouterFunctionMappingDescriptionProvider());
6676
descriptionProviders = Collections.unmodifiableList(providers);
6777
}
6878

@@ -200,6 +210,62 @@ public List<DispatcherServletMappingDescription> describe(Iterable handlerMappin
200210

201211
}
202212

213+
private static final class RouterFunctionMappingDescriptionProvider
214+
implements HandlerMappingDescriptionProvider<RouterFunctionMapping> {
215+
216+
@Override
217+
public Class<RouterFunctionMapping> getMappingClass() {
218+
return RouterFunctionMapping.class;
219+
}
220+
221+
@Override
222+
public List<DispatcherServletMappingDescription> describe(RouterFunctionMapping handlerMapping) {
223+
MappingDescriptionVisitor visitor = new MappingDescriptionVisitor();
224+
RouterFunction<?> routerFunction = handlerMapping.getRouterFunction();
225+
if (routerFunction != null) {
226+
routerFunction.accept(visitor);
227+
}
228+
return visitor.descriptions;
229+
}
230+
231+
}
232+
233+
private static final class MappingDescriptionVisitor implements Visitor {
234+
235+
private final List<DispatcherServletMappingDescription> descriptions = new ArrayList<>();
236+
237+
@Override
238+
public void startNested(RequestPredicate predicate) {
239+
}
240+
241+
@Override
242+
public void endNested(RequestPredicate predicate) {
243+
}
244+
245+
@Override
246+
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
247+
DispatcherServletMappingDetails details = new DispatcherServletMappingDetails();
248+
details.setHandlerFunction(new HandlerFunctionDescription(handlerFunction));
249+
this.descriptions.add(
250+
new DispatcherServletMappingDescription(predicate.toString(), handlerFunction.toString(), details));
251+
}
252+
253+
@Override
254+
public void resources(Function<ServerRequest, Optional<Resource>> lookupFunction) {
255+
256+
}
257+
258+
@Override
259+
public void attributes(Map<String, Object> attributes) {
260+
}
261+
262+
@Override
263+
public void unknown(RouterFunction<?> routerFunction) {
264+
265+
}
266+
267+
}
268+
203269
static class DispatcherServletsMappingDescriptionProviderRuntimeHints implements RuntimeHintsRegistrar {
204270

205271
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2012-2025 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+
* https://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.web.mappings.servlet;
18+
19+
20+
import org.springframework.web.servlet.function.HandlerFunction;
21+
22+
/**
23+
* Description of a {@link HandlerFunction}.
24+
*
25+
* @author Xiong Tang
26+
* @since 3.5.0
27+
*/
28+
public class HandlerFunctionDescription {
29+
30+
private final String className;
31+
32+
HandlerFunctionDescription(HandlerFunction<?> handlerFunction) {
33+
this.className = getHandlerFunctionClassName(handlerFunction);
34+
}
35+
36+
private static String getHandlerFunctionClassName(HandlerFunction<?> handlerFunction) {
37+
Class<?> functionClass = handlerFunction.getClass();
38+
String canonicalName = functionClass.getCanonicalName();
39+
return (canonicalName != null) ? canonicalName : functionClass.getName();
40+
}
41+
42+
public String getClassName() {
43+
return this.className;
44+
}
45+
46+
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,7 +88,7 @@ void servletWebMappings() {
8888
"dispatcherServlets");
8989
assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet");
9090
List<DispatcherServletMappingDescription> handlerMappings = dispatcherServlets.get("dispatcherServlet");
91-
assertThat(handlerMappings).hasSize(1);
91+
assertThat(handlerMappings).hasSize(3);
9292
List<ServletRegistrationMappingDescription> servlets = mappings(contextMappings, "servlets");
9393
assertThat(servlets).hasSize(1);
9494
List<FilterRegistrationMappingDescription> filters = mappings(contextMappings, "servletFilters");
@@ -111,7 +111,7 @@ void servletWebMappingsWithPathPatternParser() {
111111
"dispatcherServlets");
112112
assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet");
113113
List<DispatcherServletMappingDescription> handlerMappings = dispatcherServlets.get("dispatcherServlet");
114-
assertThat(handlerMappings).hasSize(1);
114+
assertThat(handlerMappings).hasSize(3);
115115
List<ServletRegistrationMappingDescription> servlets = mappings(contextMappings, "servlets");
116116
assertThat(servlets).hasSize(1);
117117
List<FilterRegistrationMappingDescription> filters = mappings(contextMappings, "servletFilters");
@@ -131,9 +131,9 @@ void servletWebMappingsWithAdditionalDispatcherServlets() {
131131
"dispatcherServlets");
132132
assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet",
133133
"customDispatcherServletRegistration", "anotherDispatcherServletRegistration");
134-
assertThat(dispatcherServlets.get("dispatcherServlet")).hasSize(1);
135-
assertThat(dispatcherServlets.get("customDispatcherServletRegistration")).hasSize(1);
136-
assertThat(dispatcherServlets.get("anotherDispatcherServletRegistration")).hasSize(1);
134+
assertThat(dispatcherServlets.get("dispatcherServlet")).hasSize(3);
135+
assertThat(dispatcherServlets.get("customDispatcherServletRegistration")).hasSize(3);
136+
assertThat(dispatcherServlets.get("anotherDispatcherServletRegistration")).hasSize(3);
137137
});
138138
}
139139

@@ -253,6 +253,15 @@ void three() {
253253

254254
}
255255

256+
@Bean
257+
org.springframework.web.servlet.function.RouterFunction<org.springframework.web.servlet.function.ServerResponse> routerFunction() {
258+
return org.springframework.web.servlet.function.RouterFunctions
259+
.route(org.springframework.web.servlet.function.RequestPredicates.GET("/one"),
260+
(request) -> org.springframework.web.servlet.function.ServerResponse.ok().build())
261+
.andRoute(org.springframework.web.servlet.function.RequestPredicates.POST("/two"),
262+
(request) -> org.springframework.web.servlet.function.ServerResponse.ok().build());
263+
}
264+
256265
}
257266

258267
@Configuration

0 commit comments

Comments
 (0)