Skip to content

Commit

Permalink
Merge pull request #44163 from null
Browse files Browse the repository at this point in the history
* gh-44173:
  Polish "Add support for MVC router functions to mappings endpoint"
  Add support for MVC router functions to mappings endpoint

Closes gh-44163
  • Loading branch information
wilkinsona committed Feb 13, 2025
2 parents 4c097b9 + b2ba2a5 commit 682dbe9
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -47,18 +47,23 @@
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;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
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)
Expand Down Expand Up @@ -130,6 +135,13 @@ void mappings() {
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<FieldDescriptor> 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()
Expand Down Expand Up @@ -192,6 +204,11 @@ ExampleController exampleController() {
return new ExampleController();
}

@Bean
RouterFunction<ServerResponse> exampleRouter() {
return RouterFunctions.route(GET("/foo"), (request) -> ServerResponse.ok().build());
}

}

@RestController
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,12 +23,15 @@
* 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() {
Expand All @@ -39,6 +42,14 @@ void setHandlerMethod(HandlerMethodDescription handlerMethod) {
this.handlerMethod = handlerMethod;
}

public HandlerFunctionDescription getHandlerFunction() {
return this.handlerFunction;
}

void setHandlerFunction(HandlerFunctionDescription handlerFunction) {
this.handlerFunction = handlerFunction;
}

public RequestMappingConditionsDescription getRequestMappingConditions() {
return this.requestMappingConditions;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,8 @@
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;
Expand All @@ -36,10 +38,17 @@
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;
Expand All @@ -51,6 +60,7 @@
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Xiong Tang
* @since 2.0.0
*/
@ImportRuntimeHints(DispatcherServletsMappingDescriptionProviderRuntimeHints.class)
Expand All @@ -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);
}

Expand Down Expand Up @@ -200,6 +211,62 @@ public List<DispatcherServletMappingDescription> describe(Iterable handlerMappin

}

private static final class RouterFunctionMappingDescriptionProvider
implements HandlerMappingDescriptionProvider<RouterFunctionMapping> {

@Override
public Class<RouterFunctionMapping> getMappingClass() {
return RouterFunctionMapping.class;
}

@Override
public List<DispatcherServletMappingDescription> 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<DispatcherServletMappingDescription> 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<ServerRequest, Optional<Resource>> lookupFunction) {

}

@Override
public void attributes(Map<String, Object> attributes) {
}

@Override
public void unknown(RouterFunction<?> routerFunction) {

}

}

static class DispatcherServletsMappingDescriptionProviderRuntimeHints implements RuntimeHintsRegistrar {

private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -57,6 +57,8 @@
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;
Expand All @@ -71,6 +73,7 @@
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Xiong Tang
*/
class MappingsEndpointTests {

Expand All @@ -88,7 +91,7 @@ void servletWebMappings() {
"dispatcherServlets");
assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet");
List<DispatcherServletMappingDescription> handlerMappings = dispatcherServlets.get("dispatcherServlet");
assertThat(handlerMappings).hasSize(1);
assertThat(handlerMappings).hasSize(4);
List<ServletRegistrationMappingDescription> servlets = mappings(contextMappings, "servlets");
assertThat(servlets).hasSize(1);
List<FilterRegistrationMappingDescription> filters = mappings(contextMappings, "servletFilters");
Expand All @@ -111,7 +114,7 @@ void servletWebMappingsWithPathPatternParser() {
"dispatcherServlets");
assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet");
List<DispatcherServletMappingDescription> handlerMappings = dispatcherServlets.get("dispatcherServlet");
assertThat(handlerMappings).hasSize(1);
assertThat(handlerMappings).hasSize(4);
List<ServletRegistrationMappingDescription> servlets = mappings(contextMappings, "servlets");
assertThat(servlets).hasSize(1);
List<FilterRegistrationMappingDescription> filters = mappings(contextMappings, "servletFilters");
Expand All @@ -131,9 +134,9 @@ void servletWebMappingsWithAdditionalDispatcherServlets() {
"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);
});
}

Expand Down Expand Up @@ -248,11 +251,28 @@ DispatcherServlet dispatcherServlet(WebApplicationContext context) throws Servle
return dispatcherServlet;
}

@Bean
org.springframework.web.servlet.function.RouterFunction<org.springframework.web.servlet.function.ServerResponse> 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<org.springframework.web.servlet.function.ServerResponse> routerFunctionWithAttributes() {
return RouterFunctions
.route(RequestPredicates.GET("/four"),
(request) -> org.springframework.web.servlet.function.ServerResponse.ok().build())
.withAttribute("test", "test");
}

}

@Configuration
Expand Down

0 comments on commit 682dbe9

Please sign in to comment.