From a747bcf6d43e2c78afc8b3de6f131698ba4cdf8a Mon Sep 17 00:00:00 2001 From: puppy4c Date: Sat, 8 Feb 2025 21:13:54 +0800 Subject: [PATCH] Add support for MVC router functions to mappings endpoint See gh-44163 Signed-off-by: puppy4c --- ...ingsEndpointServletDocumentationTests.java | 18 ++++- .../DispatcherServletMappingDetails.java | 12 +++- ...herServletsMappingDescriptionProvider.java | 68 ++++++++++++++++++- .../servlet/HandlerFunctionDescription.java | 46 +++++++++++++ .../web/mappings/MappingsEndpointTests.java | 21 ++++-- 5 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/HandlerFunctionDescription.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java index b11040e97f8..f3838b16b3e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,9 @@ import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -54,6 +57,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.response import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; +import static org.springframework.web.servlet.function.RequestPredicates.GET; /** * Tests for generating documentation describing {@link MappingsEndpoint}. @@ -130,6 +134,13 @@ class MappingsEndpointServletDocumentationTests extends AbstractEndpointDocument fieldWithPath("*.[].details.handlerMethod.name").description("Name of the method."), fieldWithPath("*.[].details.handlerMethod.descriptor") .description("Descriptor of the method as specified in the Java Language Specification.")); + List handlerFunction = List.of( + fieldWithPath("*.[].details.handlerFunction").optional() + .type(JsonFieldType.OBJECT) + .description("Details of the function, if any, that will handle requests to this mapping."), + fieldWithPath("*.[].details.handlerFunction.className").type(JsonFieldType.STRING) + .description("Fully qualified name of the class of the function.")); + dispatcherServletFields.addAll(handlerFunction); dispatcherServletFields.addAll(handlerMethod); dispatcherServletFields.addAll(requestMappingConditions); this.client.get() @@ -192,6 +203,11 @@ class MappingsEndpointServletDocumentationTests extends AbstractEndpointDocument return new ExampleController(); } + @Bean + RouterFunction exampleRouter() { + return RouterFunctions.route(GET("/foo"), (request) -> ServerResponse.ok().build()); + } + } @RestController diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletMappingDetails.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletMappingDetails.java index 699ff05b460..05326ea5054 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletMappingDetails.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletMappingDetails.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,8 @@ public class DispatcherServletMappingDetails { private HandlerMethodDescription handlerMethod; + private HandlerFunctionDescription handlerFunction; + private RequestMappingConditionsDescription requestMappingConditions; public HandlerMethodDescription getHandlerMethod() { @@ -39,6 +41,14 @@ public class DispatcherServletMappingDetails { this.handlerMethod = handlerMethod; } + public HandlerFunctionDescription getHandlerFunction() { + return this.handlerFunction; + } + + void setHandlerFunction(HandlerFunctionDescription handlerFunction) { + this.handlerFunction = handlerFunction; + } + public RequestMappingConditionsDescription getRequestMappingConditions() { return this.requestMappingConditions; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java index ffb48304ee9..452ca506f73 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; import jakarta.servlet.Servlet; @@ -36,10 +38,17 @@ import org.springframework.boot.actuate.web.mappings.servlet.DispatcherServletsM import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.io.Resource; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RequestPredicate; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions.Visitor; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; @@ -63,6 +72,7 @@ public class DispatcherServletsMappingDescriptionProvider implements MappingDesc providers.add(new RequestMappingInfoHandlerMappingDescriptionProvider()); providers.add(new UrlHandlerMappingDescriptionProvider()); providers.add(new IterableDelegatesHandlerMappingDescriptionProvider(new ArrayList<>(providers))); + providers.add(new RouterFunctionMappingDescriptionProvider()); descriptionProviders = Collections.unmodifiableList(providers); } @@ -200,6 +210,62 @@ public class DispatcherServletsMappingDescriptionProvider implements MappingDesc } + private static final class RouterFunctionMappingDescriptionProvider + implements HandlerMappingDescriptionProvider { + + @Override + public Class getMappingClass() { + return RouterFunctionMapping.class; + } + + @Override + public List describe(RouterFunctionMapping handlerMapping) { + MappingDescriptionVisitor visitor = new MappingDescriptionVisitor(); + RouterFunction routerFunction = handlerMapping.getRouterFunction(); + if (routerFunction != null) { + routerFunction.accept(visitor); + } + return visitor.descriptions; + } + + } + + private static final class MappingDescriptionVisitor implements Visitor { + + private final List descriptions = new ArrayList<>(); + + @Override + public void startNested(RequestPredicate predicate) { + } + + @Override + public void endNested(RequestPredicate predicate) { + } + + @Override + public void route(RequestPredicate predicate, HandlerFunction handlerFunction) { + DispatcherServletMappingDetails details = new DispatcherServletMappingDetails(); + details.setHandlerFunction(new HandlerFunctionDescription(handlerFunction)); + this.descriptions.add( + new DispatcherServletMappingDescription(predicate.toString(), handlerFunction.toString(), details)); + } + + @Override + public void resources(Function> lookupFunction) { + + } + + @Override + public void attributes(Map attributes) { + } + + @Override + public void unknown(RouterFunction routerFunction) { + + } + + } + static class DispatcherServletsMappingDescriptionProviderRuntimeHints implements RuntimeHintsRegistrar { private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/HandlerFunctionDescription.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/HandlerFunctionDescription.java new file mode 100644 index 00000000000..26df168baec --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/HandlerFunctionDescription.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.web.mappings.servlet; + + +import org.springframework.web.servlet.function.HandlerFunction; + +/** + * Description of a {@link HandlerFunction}. + * + * @author Xiong Tang + * @since 3.5.0 + */ +public class HandlerFunctionDescription { + + private final String className; + + HandlerFunctionDescription(HandlerFunction handlerFunction) { + this.className = getHandlerFunctionClassName(handlerFunction); + } + + private static String getHandlerFunctionClassName(HandlerFunction handlerFunction) { + Class functionClass = handlerFunction.getClass(); + String canonicalName = functionClass.getCanonicalName(); + return (canonicalName != null) ? canonicalName : functionClass.getName(); + } + + public String getClassName() { + return this.className; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java index 929b7a9e1ff..f99745216be 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,7 +88,7 @@ class MappingsEndpointTests { "dispatcherServlets"); assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet"); List handlerMappings = dispatcherServlets.get("dispatcherServlet"); - assertThat(handlerMappings).hasSize(1); + assertThat(handlerMappings).hasSize(3); List servlets = mappings(contextMappings, "servlets"); assertThat(servlets).hasSize(1); List filters = mappings(contextMappings, "servletFilters"); @@ -111,7 +111,7 @@ class MappingsEndpointTests { "dispatcherServlets"); assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet"); List handlerMappings = dispatcherServlets.get("dispatcherServlet"); - assertThat(handlerMappings).hasSize(1); + assertThat(handlerMappings).hasSize(3); List servlets = mappings(contextMappings, "servlets"); assertThat(servlets).hasSize(1); List filters = mappings(contextMappings, "servletFilters"); @@ -131,9 +131,9 @@ class MappingsEndpointTests { "dispatcherServlets"); assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet", "customDispatcherServletRegistration", "anotherDispatcherServletRegistration"); - assertThat(dispatcherServlets.get("dispatcherServlet")).hasSize(1); - assertThat(dispatcherServlets.get("customDispatcherServletRegistration")).hasSize(1); - assertThat(dispatcherServlets.get("anotherDispatcherServletRegistration")).hasSize(1); + assertThat(dispatcherServlets.get("dispatcherServlet")).hasSize(3); + assertThat(dispatcherServlets.get("customDispatcherServletRegistration")).hasSize(3); + assertThat(dispatcherServlets.get("anotherDispatcherServletRegistration")).hasSize(3); }); } @@ -253,6 +253,15 @@ class MappingsEndpointTests { } + @Bean + org.springframework.web.servlet.function.RouterFunction routerFunction() { + return org.springframework.web.servlet.function.RouterFunctions + .route(org.springframework.web.servlet.function.RequestPredicates.GET("/one"), + (request) -> org.springframework.web.servlet.function.ServerResponse.ok().build()) + .andRoute(org.springframework.web.servlet.function.RequestPredicates.POST("/two"), + (request) -> org.springframework.web.servlet.function.ServerResponse.ok().build()); + } + } @Configuration