Skip to content

Commit 17c7f02

Browse files
committed
Add details of the request mapping conditions to mappings endpoint
Closes spring-projectsgh-12080
1 parent 2c19257 commit 17c7f02

File tree

9 files changed

+516
-76
lines changed

9 files changed

+516
-76
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ When using Spring MVC, the response contains details of any `DispatcherServlet`
4040
request mappings beneath `contexts.*.mappings.dispatcherServlets`. The following
4141
table describes the structure of this section of the response:
4242

43-
[cols="3,1,3"]
43+
[cols="4,1,2"]
4444
include::{snippets}mappings/response-fields-dispatcher-servlets.adoc[]
4545

4646

@@ -76,5 +76,5 @@ When using Spring WebFlux, the response contains details of any `DispatcherHandl
7676
request mappings beneath `contexts.*.mappings.dispatcherHandlers`. The following
7777
table describes the structure of this section of the response:
7878

79-
[cols="3,1,3"]
79+
[cols="4,1,2"]
8080
include::{snippets}mappings/response-fields-dispatcher-handlers.adoc[]

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

+99-37
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
1818

19+
import java.util.ArrayList;
20+
import java.util.Arrays;
1921
import java.util.Collection;
22+
import java.util.List;
2023

2124
import org.junit.Before;
2225
import org.junit.Rule;
@@ -34,10 +37,14 @@
3437
import org.springframework.context.annotation.Bean;
3538
import org.springframework.context.annotation.Configuration;
3639
import org.springframework.context.annotation.Import;
40+
import org.springframework.http.MediaType;
3741
import org.springframework.restdocs.JUnitRestDocumentation;
42+
import org.springframework.restdocs.payload.FieldDescriptor;
3843
import org.springframework.restdocs.payload.JsonFieldType;
3944
import org.springframework.test.context.junit4.SpringRunner;
4045
import org.springframework.test.web.reactive.server.WebTestClient;
46+
import org.springframework.web.bind.annotation.PostMapping;
47+
import org.springframework.web.bind.annotation.RestController;
4148
import org.springframework.web.reactive.function.server.RequestPredicates;
4249
import org.springframework.web.reactive.function.server.RouterFunction;
4350
import org.springframework.web.reactive.function.server.RouterFunctions;
@@ -77,49 +84,88 @@ public void webTestClient() {
7784

7885
@Test
7986
public void mappings() throws Exception {
87+
List<FieldDescriptor> requestMappingConditions = Arrays.asList(
88+
requestMappingConditionField("")
89+
.description("Details of the request mapping conditions.")
90+
.optional(),
91+
requestMappingConditionField(".consumes")
92+
.description("Details of the consumes condition"),
93+
requestMappingConditionField(".consumes.[].mediaType")
94+
.description("Consumed media type."),
95+
requestMappingConditionField(".consumes.[].negated")
96+
.description("Whether the media type is negated."),
97+
requestMappingConditionField(".headers")
98+
.description("Details of the headers condition."),
99+
requestMappingConditionField(".headers.[].name")
100+
.description("Name of the header."),
101+
requestMappingConditionField(".headers.[].value")
102+
.description("Required value of the header, if any."),
103+
requestMappingConditionField(".headers.[].negated")
104+
.description("Whether the value is negated."),
105+
requestMappingConditionField(".methods")
106+
.description("HTTP methods that are handled."),
107+
requestMappingConditionField(".params")
108+
.description("Details of the params condition."),
109+
requestMappingConditionField(".params.[].name")
110+
.description("Name of the parameter."),
111+
requestMappingConditionField(".params.[].value")
112+
.description("Required value of the parameter, if any."),
113+
requestMappingConditionField(".params.[].negated")
114+
.description("Whether the value is negated."),
115+
requestMappingConditionField(".patterns").description(
116+
"Patterns identifying the paths handled by the mapping."),
117+
requestMappingConditionField(".produces")
118+
.description("Details of the produces condition."),
119+
requestMappingConditionField(".produces.[].mediaType")
120+
.description("Produced media type."),
121+
requestMappingConditionField(".produces.[].negated")
122+
.description("Whether the media type is negated."));
123+
List<FieldDescriptor> handlerMethod = Arrays.asList(
124+
fieldWithPath("*.[].details.handlerMethod").optional()
125+
.type(JsonFieldType.OBJECT)
126+
.description("Details of the method, if any, "
127+
+ "that will handle requests to this mapping."),
128+
fieldWithPath("*.[].details.handlerMethod.className")
129+
.type(JsonFieldType.STRING)
130+
.description("Fully qualified name of the class of the method."),
131+
fieldWithPath("*.[].details.handlerMethod.name")
132+
.type(JsonFieldType.STRING).description("Name of the method."),
133+
fieldWithPath("*.[].details.handlerMethod.descriptor")
134+
.type(JsonFieldType.STRING)
135+
.description("Descriptor of the method as specified in the Java "
136+
+ "Language Specification."));
137+
List<FieldDescriptor> handlerFunction = Arrays.asList(
138+
fieldWithPath("*.[].details.handlerFunction").optional()
139+
.type(JsonFieldType.OBJECT)
140+
.description("Details of the function, if any, that will handle "
141+
+ "requests to this mapping."),
142+
fieldWithPath("*.[].details.handlerFunction.className")
143+
.type(JsonFieldType.STRING).description(
144+
"Fully qualified name of the class of the function."));
145+
List<FieldDescriptor> dispatcherHandlerFields = new ArrayList<>(Arrays.asList(
146+
fieldWithPath("*")
147+
.description("Dispatcher handler mappings, if any, keyed by "
148+
+ "dispatcher handler bean name."),
149+
fieldWithPath("*.[].details").optional().type(JsonFieldType.OBJECT)
150+
.description("Additional implementation-specific "
151+
+ "details about the mapping. Optional."),
152+
fieldWithPath("*.[].handler").description("Handler for the mapping."),
153+
fieldWithPath("*.[].predicate")
154+
.description("Predicate for the mapping.")));
155+
dispatcherHandlerFields.addAll(requestMappingConditions);
156+
dispatcherHandlerFields.addAll(handlerMethod);
157+
dispatcherHandlerFields.addAll(handlerFunction);
80158
this.client.get().uri("/actuator/mappings").exchange().expectStatus().isOk()
81159
.expectBody()
82160
.consumeWith(document("mappings",
83161
responseFields(
84162
beneathPath("contexts.*.mappings.dispatcherHandlers")
85163
.withSubsectionId("dispatcher-handlers"),
86-
fieldWithPath("*").description(
87-
"Dispatcher handler mappings, if any, keyed by "
88-
+ "dispatcher handler bean name."),
89-
fieldWithPath("*.[].handler")
90-
.description("Handler for the mapping."),
91-
fieldWithPath("*.[].predicate")
92-
.description("Predicate for the mapping."),
93-
fieldWithPath("*.[].details").optional()
94-
.type(JsonFieldType.OBJECT)
95-
.description("Additional implementation-specific "
96-
+ "details about the mapping. Optional."),
97-
fieldWithPath("*.[].details.handlerMethod").optional()
98-
.type(JsonFieldType.OBJECT)
99-
.description("Details of the method, if any, "
100-
+ "that will handle requests to "
101-
+ "this mapping."),
102-
fieldWithPath("*.[].details.handlerMethod.className")
103-
.type(JsonFieldType.STRING)
104-
.description("Fully qualified name of the class"
105-
+ " of the method."),
106-
fieldWithPath("*.[].details.handlerMethod.name")
107-
.type(JsonFieldType.STRING)
108-
.description("Name of the method."),
109-
fieldWithPath("*.[].details.handlerMethod.descriptor")
110-
.type(JsonFieldType.STRING)
111-
.description("Descriptor of the method as "
112-
+ "specified in the Java Language "
113-
+ "Specification."),
114-
fieldWithPath("*.[].details.handlerFunction")
115-
.optional().type(JsonFieldType.OBJECT)
116-
.description("Details of the function, if any, "
117-
+ "that will handle requests to this "
118-
+ "mapping."),
119-
fieldWithPath("*.[].details.handlerFunction.className")
120-
.type(JsonFieldType.STRING).description(
121-
"Fully qualified name of the class of "
122-
+ "the function."))));
164+
dispatcherHandlerFields)));
165+
}
166+
167+
private FieldDescriptor requestMappingConditionField(String path) {
168+
return fieldWithPath("*.[].details.requestMappingConditions" + path);
123169
}
124170

125171
@Configuration
@@ -149,6 +195,22 @@ public RouterFunction<ServerResponse> exampleRouter() {
149195
(request) -> ServerResponse.ok().build());
150196
}
151197

198+
@Bean
199+
public ExampleController exampleController() {
200+
return new ExampleController();
201+
}
202+
203+
}
204+
205+
@RestController
206+
private static class ExampleController {
207+
208+
@PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE,
209+
"!application/xml" }, produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha")
210+
public String example() {
211+
return "Hello World";
212+
}
213+
152214
}
153215

154216
}

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

+94-31
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
1818

19+
import java.util.ArrayList;
20+
import java.util.Arrays;
1921
import java.util.Collection;
22+
import java.util.List;
2023

2124
import org.junit.Before;
2225
import org.junit.Rule;
@@ -36,11 +39,15 @@
3639
import org.springframework.context.annotation.Bean;
3740
import org.springframework.context.annotation.Configuration;
3841
import org.springframework.context.annotation.Import;
42+
import org.springframework.http.MediaType;
3943
import org.springframework.restdocs.JUnitRestDocumentation;
44+
import org.springframework.restdocs.payload.FieldDescriptor;
4045
import org.springframework.restdocs.payload.JsonFieldType;
4146
import org.springframework.restdocs.payload.ResponseFieldsSnippet;
4247
import org.springframework.test.context.junit4.SpringRunner;
4348
import org.springframework.test.web.reactive.server.WebTestClient;
49+
import org.springframework.web.bind.annotation.PostMapping;
50+
import org.springframework.web.bind.annotation.RestController;
4451

4552
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
4653
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
@@ -91,39 +98,75 @@ public void mappings() throws Exception {
9198
.description("Dispatcher handler mappings, if any.").optional()
9299
.type(JsonFieldType.OBJECT),
93100
parentIdField());
101+
List<FieldDescriptor> dispatcherServletFields = new ArrayList<>(Arrays.asList(
102+
fieldWithPath("*")
103+
.description("Dispatcher servlet mappings, if any, keyed by "
104+
+ "dispatcher servlet bean name."),
105+
fieldWithPath("*.[].details").optional().type(JsonFieldType.OBJECT)
106+
.description("Additional implementation-specific "
107+
+ "details about the mapping. Optional."),
108+
fieldWithPath("*.[].handler").description("Handler for the mapping."),
109+
fieldWithPath("*.[].predicate")
110+
.description("Predicate for the mapping.")));
111+
List<FieldDescriptor> requestMappingConditions = Arrays.asList(
112+
requestMappingConditionField("")
113+
.description("Details of the request mapping conditions.")
114+
.optional(),
115+
requestMappingConditionField(".consumes")
116+
.description("Details of the consumes condition"),
117+
requestMappingConditionField(".consumes.[].mediaType")
118+
.description("Consumed media type."),
119+
requestMappingConditionField(".consumes.[].negated")
120+
.description("Whether the media type is negated."),
121+
requestMappingConditionField(".headers")
122+
.description("Details of the headers condition."),
123+
requestMappingConditionField(".headers.[].name")
124+
.description("Name of the header."),
125+
requestMappingConditionField(".headers.[].value")
126+
.description("Required value of the header, if any."),
127+
requestMappingConditionField(".headers.[].negated")
128+
.description("Whether the value is negated."),
129+
requestMappingConditionField(".methods")
130+
.description("HTTP methods that are handled."),
131+
requestMappingConditionField(".params")
132+
.description("Details of the params condition."),
133+
requestMappingConditionField(".params.[].name")
134+
.description("Name of the parameter."),
135+
requestMappingConditionField(".params.[].value")
136+
.description("Required value of the parameter, if any."),
137+
requestMappingConditionField(".params.[].negated")
138+
.description("Whether the value is negated."),
139+
requestMappingConditionField(".patterns").description(
140+
"Patterns identifying the paths handled by the mapping."),
141+
requestMappingConditionField(".produces")
142+
.description("Details of the produces condition."),
143+
requestMappingConditionField(".produces.[].mediaType")
144+
.description("Produced media type."),
145+
requestMappingConditionField(".produces.[].negated")
146+
.description("Whether the media type is negated."));
147+
List<FieldDescriptor> handlerMethod = Arrays.asList(
148+
fieldWithPath("*.[].details.handlerMethod").optional()
149+
.type(JsonFieldType.OBJECT)
150+
.description("Details of the method, if any, "
151+
+ "that will handle requests to this mapping."),
152+
fieldWithPath("*.[].details.handlerMethod.className")
153+
.type(JsonFieldType.STRING)
154+
.description("Fully qualified name of the class of the method."),
155+
fieldWithPath("*.[].details.handlerMethod.name")
156+
.type(JsonFieldType.STRING).description("Name of the method."),
157+
fieldWithPath("*.[].details.handlerMethod.descriptor")
158+
.type(JsonFieldType.STRING)
159+
.description("Descriptor of the method as specified in the Java "
160+
+ "Language Specification."));
161+
dispatcherServletFields.addAll(handlerMethod);
162+
dispatcherServletFields.addAll(requestMappingConditions);
94163
this.client.get().uri("/actuator/mappings").exchange().expectBody()
95-
.consumeWith(document("mappings", commonResponseFields,
96-
responseFields(
97-
beneathPath("contexts.*.mappings.dispatcherServlets")
164+
.consumeWith(document(
165+
"mappings", commonResponseFields,
166+
responseFields(beneathPath(
167+
"contexts.*.mappings.dispatcherServlets")
98168
.withSubsectionId("dispatcher-servlets"),
99-
fieldWithPath("*").description(
100-
"Dispatcher servlet mappings, if any, keyed by "
101-
+ "dispatcher servlet bean name."),
102-
fieldWithPath("*.[].handler")
103-
.description("Handler for the mapping."),
104-
fieldWithPath("*.[].predicate")
105-
.description("Predicate for the mapping."),
106-
fieldWithPath("*.[].details").optional()
107-
.type(JsonFieldType.OBJECT)
108-
.description("Additional implementation-specific "
109-
+ "details about the mapping. Optional."),
110-
fieldWithPath("*.[].details.handlerMethod").optional()
111-
.type(JsonFieldType.OBJECT)
112-
.description("Details of the method, if any, "
113-
+ "that will handle requests to "
114-
+ "this mapping."),
115-
fieldWithPath("*.[].details.handlerMethod.className")
116-
.type(JsonFieldType.STRING)
117-
.description("Fully qualified name of the class"
118-
+ " of the method."),
119-
fieldWithPath("*.[].details.handlerMethod.name")
120-
.type(JsonFieldType.STRING)
121-
.description("Name of the method."),
122-
fieldWithPath("*.[].details.handlerMethod.descriptor")
123-
.type(JsonFieldType.STRING)
124-
.description("Descriptor of the method as "
125-
+ "specified in the Java Language "
126-
+ "Specification.")),
169+
dispatcherServletFields),
127170
responseFields(
128171
beneathPath("contexts.*.mappings.servletFilters")
129172
.withSubsectionId("servlet-filters"),
@@ -146,6 +189,10 @@ public void mappings() throws Exception {
146189
.description("Class name of the servlet"))));
147190
}
148191

192+
private FieldDescriptor requestMappingConditionField(String path) {
193+
return fieldWithPath("*.[].details.requestMappingConditions" + path);
194+
}
195+
149196
@Configuration
150197
@Import(BaseDocumentationConfiguration.class)
151198
static class TestConfiguration {
@@ -177,6 +224,22 @@ public MappingsEndpoint mappingsEndpoint(
177224
return new MappingsEndpoint(descriptionProviders, context);
178225
}
179226

227+
@Bean
228+
public ExampleController exampleController() {
229+
return new ExampleController();
230+
}
231+
232+
}
233+
234+
@RestController
235+
private static class ExampleController {
236+
237+
@PostMapping(path = "/", consumes = { MediaType.APPLICATION_JSON_VALUE,
238+
"!application/xml" }, produces = MediaType.TEXT_PLAIN_VALUE, headers = "X-Custom=Foo", params = "a!=alpha")
239+
public String example() {
240+
return "Hello World";
241+
}
242+
180243
}
181244

182245
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ private DispatcherHandlerMappingDescription describe(
125125
DispatcherHandlerMappingDetails handlerMapping = new DispatcherHandlerMappingDetails();
126126
handlerMapping
127127
.setHandlerMethod(new HandlerMethodDescription(mapping.getValue()));
128+
handlerMapping.setRequestMappingConditions(
129+
new RequestMappingConditionsDescription(mapping.getKey()));
128130
return new DispatcherHandlerMappingDescription(mapping.getKey().toString(),
129131
mapping.getValue().toString(), handlerMapping);
130132
}

0 commit comments

Comments
 (0)