From 17c7f027e02d50ea3f0fa8d71c7b84ed34f4046a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 Feb 2018 16:53:11 +0000 Subject: [PATCH] Add details of the request mapping conditions to mappings endpoint Closes gh-12080 --- .../src/main/asciidoc/endpoints/mappings.adoc | 4 +- ...ngsEndpointReactiveDocumentationTests.java | 136 ++++++++++++----- ...ingsEndpointServletDocumentationTests.java | 125 +++++++++++---- ...herHandlersMappingDescriptionProvider.java | 2 + .../RequestMappingConditionsDescription.java | 142 ++++++++++++++++++ .../DispatcherServletMappingDetails.java | 19 ++- ...herServletsMappingDescriptionProvider.java | 8 +- .../RequestMappingConditionsDescription.java | 140 +++++++++++++++++ .../sample/webflux/ExampleController.java | 16 ++ 9 files changed, 516 insertions(+), 76 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/RequestMappingConditionsDescription.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java create mode 100644 spring-boot-samples/spring-boot-sample-webflux/src/main/java/sample/webflux/ExampleController.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc index b2c6b6d5d4..edf2a92286 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc @@ -40,7 +40,7 @@ When using Spring MVC, the response contains details of any `DispatcherServlet` request mappings beneath `contexts.*.mappings.dispatcherServlets`. The following table describes the structure of this section of the response: -[cols="3,1,3"] +[cols="4,1,2"] include::{snippets}mappings/response-fields-dispatcher-servlets.adoc[] @@ -76,5 +76,5 @@ When using Spring WebFlux, the response contains details of any `DispatcherHandl request mappings beneath `contexts.*.mappings.dispatcherHandlers`. The following table describes the structure of this section of the response: -[cols="3,1,3"] +[cols="4,1,2"] include::{snippets}mappings/response-fields-dispatcher-handlers.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java index 18f20c1200..26b32e4e95 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java @@ -16,7 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.List; import org.junit.Before; import org.junit.Rule; @@ -34,10 +37,14 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.junit4.SpringRunner; 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.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; @@ -77,49 +84,88 @@ public class MappingsEndpointReactiveDocumentationTests @Test public void mappings() throws Exception { + List requestMappingConditions = Arrays.asList( + requestMappingConditionField("") + .description("Details of the request mapping conditions.") + .optional(), + requestMappingConditionField(".consumes") + .description("Details of the consumes condition"), + requestMappingConditionField(".consumes.[].mediaType") + .description("Consumed media type."), + requestMappingConditionField(".consumes.[].negated") + .description("Whether the media type is negated."), + requestMappingConditionField(".headers") + .description("Details of the headers condition."), + requestMappingConditionField(".headers.[].name") + .description("Name of the header."), + requestMappingConditionField(".headers.[].value") + .description("Required value of the header, if any."), + requestMappingConditionField(".headers.[].negated") + .description("Whether the value is negated."), + requestMappingConditionField(".methods") + .description("HTTP methods that are handled."), + requestMappingConditionField(".params") + .description("Details of the params condition."), + requestMappingConditionField(".params.[].name") + .description("Name of the parameter."), + requestMappingConditionField(".params.[].value") + .description("Required value of the parameter, if any."), + requestMappingConditionField(".params.[].negated") + .description("Whether the value is negated."), + requestMappingConditionField(".patterns").description( + "Patterns identifying the paths handled by the mapping."), + requestMappingConditionField(".produces") + .description("Details of the produces condition."), + requestMappingConditionField(".produces.[].mediaType") + .description("Produced media type."), + requestMappingConditionField(".produces.[].negated") + .description("Whether the media type is negated.")); + List handlerMethod = Arrays.asList( + fieldWithPath("*.[].details.handlerMethod").optional() + .type(JsonFieldType.OBJECT) + .description("Details of the method, if any, " + + "that will handle requests to this mapping."), + fieldWithPath("*.[].details.handlerMethod.className") + .type(JsonFieldType.STRING) + .description("Fully qualified name of the class of the method."), + fieldWithPath("*.[].details.handlerMethod.name") + .type(JsonFieldType.STRING).description("Name of the method."), + fieldWithPath("*.[].details.handlerMethod.descriptor") + .type(JsonFieldType.STRING) + .description("Descriptor of the method as specified in the Java " + + "Language Specification.")); + List handlerFunction = Arrays.asList( + 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.")); + List dispatcherHandlerFields = new ArrayList<>(Arrays.asList( + fieldWithPath("*") + .description("Dispatcher handler mappings, if any, keyed by " + + "dispatcher handler bean name."), + fieldWithPath("*.[].details").optional().type(JsonFieldType.OBJECT) + .description("Additional implementation-specific " + + "details about the mapping. Optional."), + fieldWithPath("*.[].handler").description("Handler for the mapping."), + fieldWithPath("*.[].predicate") + .description("Predicate for the mapping."))); + dispatcherHandlerFields.addAll(requestMappingConditions); + dispatcherHandlerFields.addAll(handlerMethod); + dispatcherHandlerFields.addAll(handlerFunction); this.client.get().uri("/actuator/mappings").exchange().expectStatus().isOk() .expectBody() .consumeWith(document("mappings", responseFields( beneathPath("contexts.*.mappings.dispatcherHandlers") .withSubsectionId("dispatcher-handlers"), - fieldWithPath("*").description( - "Dispatcher handler mappings, if any, keyed by " - + "dispatcher handler bean name."), - fieldWithPath("*.[].handler") - .description("Handler for the mapping."), - fieldWithPath("*.[].predicate") - .description("Predicate for the mapping."), - fieldWithPath("*.[].details").optional() - .type(JsonFieldType.OBJECT) - .description("Additional implementation-specific " - + "details about the mapping. Optional."), - fieldWithPath("*.[].details.handlerMethod").optional() - .type(JsonFieldType.OBJECT) - .description("Details of the method, if any, " - + "that will handle requests to " - + "this mapping."), - fieldWithPath("*.[].details.handlerMethod.className") - .type(JsonFieldType.STRING) - .description("Fully qualified name of the class" - + " of the method."), - fieldWithPath("*.[].details.handlerMethod.name") - .type(JsonFieldType.STRING) - .description("Name of the method."), - fieldWithPath("*.[].details.handlerMethod.descriptor") - .type(JsonFieldType.STRING) - .description("Descriptor of the method as " - + "specified in the Java Language " - + "Specification."), - 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.")))); + dispatcherHandlerFields))); + } + + private FieldDescriptor requestMappingConditionField(String path) { + return fieldWithPath("*.[].details.requestMappingConditions" + path); } @Configuration @@ -149,6 +195,22 @@ public class MappingsEndpointReactiveDocumentationTests (request) -> ServerResponse.ok().build()); } + @Bean + public ExampleController exampleController() { + return new ExampleController(); + } + + } + + @RestController + private static class ExampleController { + + @PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE, + "!application/xml" }, produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha") + public String example() { + return "Hello World"; + } + } } 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 a382a70a76..077e5d0f83 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 @@ -16,7 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.List; import org.junit.Before; import org.junit.Rule; @@ -36,11 +39,15 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -91,39 +98,75 @@ public class MappingsEndpointServletDocumentationTests .description("Dispatcher handler mappings, if any.").optional() .type(JsonFieldType.OBJECT), parentIdField()); + List dispatcherServletFields = new ArrayList<>(Arrays.asList( + fieldWithPath("*") + .description("Dispatcher servlet mappings, if any, keyed by " + + "dispatcher servlet bean name."), + fieldWithPath("*.[].details").optional().type(JsonFieldType.OBJECT) + .description("Additional implementation-specific " + + "details about the mapping. Optional."), + fieldWithPath("*.[].handler").description("Handler for the mapping."), + fieldWithPath("*.[].predicate") + .description("Predicate for the mapping."))); + List requestMappingConditions = Arrays.asList( + requestMappingConditionField("") + .description("Details of the request mapping conditions.") + .optional(), + requestMappingConditionField(".consumes") + .description("Details of the consumes condition"), + requestMappingConditionField(".consumes.[].mediaType") + .description("Consumed media type."), + requestMappingConditionField(".consumes.[].negated") + .description("Whether the media type is negated."), + requestMappingConditionField(".headers") + .description("Details of the headers condition."), + requestMappingConditionField(".headers.[].name") + .description("Name of the header."), + requestMappingConditionField(".headers.[].value") + .description("Required value of the header, if any."), + requestMappingConditionField(".headers.[].negated") + .description("Whether the value is negated."), + requestMappingConditionField(".methods") + .description("HTTP methods that are handled."), + requestMappingConditionField(".params") + .description("Details of the params condition."), + requestMappingConditionField(".params.[].name") + .description("Name of the parameter."), + requestMappingConditionField(".params.[].value") + .description("Required value of the parameter, if any."), + requestMappingConditionField(".params.[].negated") + .description("Whether the value is negated."), + requestMappingConditionField(".patterns").description( + "Patterns identifying the paths handled by the mapping."), + requestMappingConditionField(".produces") + .description("Details of the produces condition."), + requestMappingConditionField(".produces.[].mediaType") + .description("Produced media type."), + requestMappingConditionField(".produces.[].negated") + .description("Whether the media type is negated.")); + List handlerMethod = Arrays.asList( + fieldWithPath("*.[].details.handlerMethod").optional() + .type(JsonFieldType.OBJECT) + .description("Details of the method, if any, " + + "that will handle requests to this mapping."), + fieldWithPath("*.[].details.handlerMethod.className") + .type(JsonFieldType.STRING) + .description("Fully qualified name of the class of the method."), + fieldWithPath("*.[].details.handlerMethod.name") + .type(JsonFieldType.STRING).description("Name of the method."), + fieldWithPath("*.[].details.handlerMethod.descriptor") + .type(JsonFieldType.STRING) + .description("Descriptor of the method as specified in the Java " + + "Language Specification.")); + dispatcherServletFields.addAll(handlerMethod); + dispatcherServletFields.addAll(requestMappingConditions); this.client.get().uri("/actuator/mappings").exchange().expectBody() - .consumeWith(document("mappings", commonResponseFields, - responseFields( - beneathPath("contexts.*.mappings.dispatcherServlets") + .consumeWith(document( + "mappings", commonResponseFields, + responseFields(beneathPath( + "contexts.*.mappings.dispatcherServlets") .withSubsectionId("dispatcher-servlets"), - fieldWithPath("*").description( - "Dispatcher servlet mappings, if any, keyed by " - + "dispatcher servlet bean name."), - fieldWithPath("*.[].handler") - .description("Handler for the mapping."), - fieldWithPath("*.[].predicate") - .description("Predicate for the mapping."), - fieldWithPath("*.[].details").optional() - .type(JsonFieldType.OBJECT) - .description("Additional implementation-specific " - + "details about the mapping. Optional."), - fieldWithPath("*.[].details.handlerMethod").optional() - .type(JsonFieldType.OBJECT) - .description("Details of the method, if any, " - + "that will handle requests to " - + "this mapping."), - fieldWithPath("*.[].details.handlerMethod.className") - .type(JsonFieldType.STRING) - .description("Fully qualified name of the class" - + " of the method."), - fieldWithPath("*.[].details.handlerMethod.name") - .type(JsonFieldType.STRING) - .description("Name of the method."), - fieldWithPath("*.[].details.handlerMethod.descriptor") - .type(JsonFieldType.STRING) - .description("Descriptor of the method as " - + "specified in the Java Language " - + "Specification.")), + dispatcherServletFields), responseFields( beneathPath("contexts.*.mappings.servletFilters") .withSubsectionId("servlet-filters"), @@ -146,6 +189,10 @@ public class MappingsEndpointServletDocumentationTests .description("Class name of the servlet")))); } + private FieldDescriptor requestMappingConditionField(String path) { + return fieldWithPath("*.[].details.requestMappingConditions" + path); + } + @Configuration @Import(BaseDocumentationConfiguration.class) static class TestConfiguration { @@ -177,6 +224,22 @@ public class MappingsEndpointServletDocumentationTests return new MappingsEndpoint(descriptionProviders, context); } + @Bean + public ExampleController exampleController() { + return new ExampleController(); + } + + } + + @RestController + private static class ExampleController { + + @PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE, + "!application/xml" }, produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha") + public String example() { + return "Hello World"; + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java index 8374219bd7..d2406dc4a8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java @@ -125,6 +125,8 @@ public class DispatcherHandlersMappingDescriptionProvider DispatcherHandlerMappingDetails handlerMapping = new DispatcherHandlerMappingDetails(); handlerMapping .setHandlerMethod(new HandlerMethodDescription(mapping.getValue())); + handlerMapping.setRequestMappingConditions( + new RequestMappingConditionsDescription(mapping.getKey())); return new DispatcherHandlerMappingDescription(mapping.getKey().toString(), mapping.getValue().toString(), handlerMapping); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/RequestMappingConditionsDescription.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/RequestMappingConditionsDescription.java new file mode 100644 index 0000000000..a702e48312 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/RequestMappingConditionsDescription.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.reactive; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.result.condition.MediaTypeExpression; +import org.springframework.web.reactive.result.condition.NameValueExpression; +import org.springframework.web.reactive.result.method.RequestMappingInfo; +import org.springframework.web.util.pattern.PathPattern; + +/** + * Description of the conditions of a {@link RequestMappingInfo}. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class RequestMappingConditionsDescription { + + private final List consumes; + + private final List headers; + + private final Set methods; + + private final List params; + + private final Set patterns; + + private final List produces; + + RequestMappingConditionsDescription(RequestMappingInfo requestMapping) { + this.consumes = requestMapping.getConsumesCondition().getExpressions().stream() + .map(MediaTypeExpressionDescription::new).collect(Collectors.toList()); + this.headers = requestMapping.getHeadersCondition().getExpressions().stream() + .map(NameValueExpressionDescription::new).collect(Collectors.toList()); + this.methods = requestMapping.getMethodsCondition().getMethods(); + this.params = requestMapping.getParamsCondition().getExpressions().stream() + .map(NameValueExpressionDescription::new).collect(Collectors.toList()); + this.patterns = requestMapping.getPatternsCondition().getPatterns().stream() + .map(PathPattern::getPatternString).collect(Collectors.toSet()); + this.produces = requestMapping.getProducesCondition().getExpressions().stream() + .map(MediaTypeExpressionDescription::new).collect(Collectors.toList()); + } + + public List getConsumes() { + return this.consumes; + } + + public List getHeaders() { + return this.headers; + } + + public Set getMethods() { + return this.methods; + } + + public List getParams() { + return this.params; + } + + public Set getPatterns() { + return this.patterns; + } + + public List getProduces() { + return this.produces; + } + + /** + * A description of a {@link MediaTypeExpression} in a request mapping condition. + */ + public static class MediaTypeExpressionDescription { + + private final String mediaType; + + private final boolean negated; + + MediaTypeExpressionDescription(MediaTypeExpression expression) { + this.mediaType = expression.getMediaType().toString(); + this.negated = expression.isNegated(); + } + + public String getMediaType() { + return this.mediaType; + } + + public boolean isNegated() { + return this.negated; + } + + } + + /** + * A description of a {@link NameValueExpression} in a request mapping condition. + */ + public static class NameValueExpressionDescription { + + private final String name; + + private final Object value; + + private final boolean negated; + + NameValueExpressionDescription(NameValueExpression expression) { + this.name = expression.getName(); + this.value = expression.getValue(); + this.negated = expression.isNegated(); + } + + public String getName() { + return this.name; + } + + public Object getValue() { + return this.value; + } + + public boolean isNegated() { + return this.negated; + } + + } + +} 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 0ec7c4ebde..6a81b9915b 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 @@ -27,14 +27,25 @@ import org.springframework.web.servlet.DispatcherServlet; */ public class DispatcherServletMappingDetails { - private final HandlerMethodDescription handlerMethod; + private HandlerMethodDescription handlerMethod; - DispatcherServletMappingDetails(HandlerMethodDescription handlerMethod) { - this.handlerMethod = handlerMethod; - } + private RequestMappingConditionsDescription requestMappingConditions; public HandlerMethodDescription getHandlerMethod() { return this.handlerMethod; } + void setHandlerMethod(HandlerMethodDescription handlerMethod) { + this.handlerMethod = handlerMethod; + } + + public RequestMappingConditionsDescription getRequestMappingConditions() { + return this.requestMappingConditions; + } + + void setRequestMappingConditions( + RequestMappingConditionsDescription requestMappingConditions) { + this.requestMappingConditions = 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 5ad601adcc..37c46545e9 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 @@ -140,9 +140,13 @@ public class DispatcherServletsMappingDescriptionProvider private DispatcherServletMappingDescription describe( Entry mapping) { + DispatcherServletMappingDetails mappingDetails = new DispatcherServletMappingDetails(); + mappingDetails + .setHandlerMethod(new HandlerMethodDescription(mapping.getValue())); + mappingDetails.setRequestMappingConditions( + new RequestMappingConditionsDescription(mapping.getKey())); return new DispatcherServletMappingDescription(mapping.getKey().toString(), - mapping.getValue().toString(), new DispatcherServletMappingDetails( - new HandlerMethodDescription(mapping.getValue()))); + mapping.getValue().toString(), mappingDetails); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java new file mode 100644 index 0000000000..016c257bd2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2018 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 + * + * http://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 java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.condition.MediaTypeExpression; +import org.springframework.web.servlet.mvc.condition.NameValueExpression; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; + +/** + * Description of the conditions of a {@link RequestMappingInfo}. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class RequestMappingConditionsDescription { + + private final List consumes; + + private final List headers; + + private final Set methods; + + private final List params; + + private final Set patterns; + + private final List produces; + + RequestMappingConditionsDescription(RequestMappingInfo requestMapping) { + this.consumes = requestMapping.getConsumesCondition().getExpressions().stream() + .map(MediaTypeExpressionDescription::new).collect(Collectors.toList()); + this.headers = requestMapping.getHeadersCondition().getExpressions().stream() + .map(NameValueExpressionDescription::new).collect(Collectors.toList()); + this.methods = requestMapping.getMethodsCondition().getMethods(); + this.params = requestMapping.getParamsCondition().getExpressions().stream() + .map(NameValueExpressionDescription::new).collect(Collectors.toList()); + this.patterns = requestMapping.getPatternsCondition().getPatterns(); + this.produces = requestMapping.getProducesCondition().getExpressions().stream() + .map(MediaTypeExpressionDescription::new).collect(Collectors.toList()); + } + + public List getConsumes() { + return this.consumes; + } + + public List getHeaders() { + return this.headers; + } + + public Set getMethods() { + return this.methods; + } + + public List getParams() { + return this.params; + } + + public Set getPatterns() { + return this.patterns; + } + + public List getProduces() { + return this.produces; + } + + /** + * A description of a {@link MediaTypeExpression} in a request mapping condition. + */ + public static class MediaTypeExpressionDescription { + + private final String mediaType; + + private final boolean negated; + + MediaTypeExpressionDescription(MediaTypeExpression expression) { + this.mediaType = expression.getMediaType().toString(); + this.negated = expression.isNegated(); + } + + public String getMediaType() { + return this.mediaType; + } + + public boolean isNegated() { + return this.negated; + } + + } + + /** + * A description of a {@link NameValueExpression} in a request mapping condition. + */ + public static class NameValueExpressionDescription { + + private final String name; + + private final Object value; + + private final boolean negated; + + NameValueExpressionDescription(NameValueExpression expression) { + this.name = expression.getName(); + this.value = expression.getValue(); + this.negated = expression.isNegated(); + } + + public String getName() { + return this.name; + } + + public Object getValue() { + return this.value; + } + + public boolean isNegated() { + return this.negated; + } + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-webflux/src/main/java/sample/webflux/ExampleController.java b/spring-boot-samples/spring-boot-sample-webflux/src/main/java/sample/webflux/ExampleController.java new file mode 100644 index 0000000000..2751508819 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-webflux/src/main/java/sample/webflux/ExampleController.java @@ -0,0 +1,16 @@ +package sample.webflux; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ExampleController { + + @PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE, + "!application/xml" }, produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha") + public String example() { + return "Hello World"; + } + +}