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 b11040e97f..6ed5e3598c 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,11 +57,13 @@ 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}. * * @author Andy Wilkinson + * @author Xiong Tang */ @ExtendWith(RestDocumentationExtension.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -130,6 +135,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 +204,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 699ff05b46..786ddc5519 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. @@ -23,12 +23,15 @@ import org.springframework.web.servlet.DispatcherServlet; * Details of a {@link DispatcherServlet} mapping. * * @author Andy Wilkinson + * @author Xiong Tang * @since 2.0.0 */ public class DispatcherServletMappingDetails { private HandlerMethodDescription handlerMethod; + private HandlerFunctionDescription handlerFunction; + private RequestMappingConditionsDescription requestMappingConditions; public HandlerMethodDescription getHandlerMethod() { @@ -39,6 +42,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 ffb48304ee..2225550c1b 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; @@ -51,6 +60,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Xiong Tang * @since 2.0.0 */ @ImportRuntimeHints(DispatcherServletsMappingDescriptionProviderRuntimeHints.class) @@ -63,6 +73,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 +211,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 0000000000..f62a4f55f4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/HandlerFunctionDescription.java @@ -0,0 +1,45 @@ +/* + * 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 929b7a9e1f..5516fa69f5 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. @@ -57,6 +57,8 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.function.RequestPredicates; +import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; @@ -71,6 +73,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Xiong Tang */ class MappingsEndpointTests { @@ -88,7 +91,7 @@ class MappingsEndpointTests { "dispatcherServlets"); assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet"); List handlerMappings = dispatcherServlets.get("dispatcherServlet"); - assertThat(handlerMappings).hasSize(1); + assertThat(handlerMappings).hasSize(4); List servlets = mappings(contextMappings, "servlets"); assertThat(servlets).hasSize(1); List filters = mappings(contextMappings, "servletFilters"); @@ -111,7 +114,7 @@ class MappingsEndpointTests { "dispatcherServlets"); assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet"); List handlerMappings = dispatcherServlets.get("dispatcherServlet"); - assertThat(handlerMappings).hasSize(1); + assertThat(handlerMappings).hasSize(4); List servlets = mappings(contextMappings, "servlets"); assertThat(servlets).hasSize(1); List filters = mappings(contextMappings, "servletFilters"); @@ -131,9 +134,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(4); + assertThat(dispatcherServlets.get("customDispatcherServletRegistration")).hasSize(4); + assertThat(dispatcherServlets.get("anotherDispatcherServletRegistration")).hasSize(4); }); } @@ -248,11 +251,28 @@ class MappingsEndpointTests { return dispatcherServlet; } + @Bean + org.springframework.web.servlet.function.RouterFunction routerFunction() { + return RouterFunctions + .route(RequestPredicates.GET("/one"), + (request) -> org.springframework.web.servlet.function.ServerResponse.ok().build()) + .andRoute(RequestPredicates.POST("/two"), + (request) -> org.springframework.web.servlet.function.ServerResponse.ok().build()); + } + @RequestMapping("/three") void three() { } + @Bean + org.springframework.web.servlet.function.RouterFunction routerFunctionWithAttributes() { + return RouterFunctions + .route(RequestPredicates.GET("/four"), + (request) -> org.springframework.web.servlet.function.ServerResponse.ok().build()) + .withAttribute("test", "test"); + } + } @Configuration