Skip to content

Commit 393081f

Browse files
committed
Enable PathPattern based matching for MVC actuators
Closes gh-24645
1 parent c83aac6 commit 393081f

File tree

7 files changed

+135
-23
lines changed

7 files changed

+135
-23
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.boot.actuate.endpoint.web.Link;
3939
import org.springframework.boot.actuate.endpoint.web.WebOperation;
4040
import org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping;
41+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
4142
import org.springframework.http.HttpStatus;
4243
import org.springframework.http.ResponseEntity;
4344
import org.springframework.web.bind.annotation.ResponseBody;
@@ -64,7 +65,8 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin
6465
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
6566
CorsConfiguration corsConfiguration, CloudFoundrySecurityInterceptor securityInterceptor,
6667
EndpointLinksResolver linksResolver) {
67-
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, true);
68+
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, true,
69+
WebMvcAutoConfiguration.pathPatternParser);
6870
this.securityInterceptor = securityInterceptor;
6971
this.linksResolver = linksResolver;
7072
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
4646
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
4747
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
48+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
4849
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4950
import org.springframework.context.annotation.Bean;
5051
import org.springframework.core.env.Environment;
@@ -82,7 +83,7 @@ public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpoint
8283
boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
8384
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
8485
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
85-
shouldRegisterLinksMapping);
86+
shouldRegisterLinksMapping, WebMvcAutoConfiguration.pathPatternParser);
8687
}
8788

8889
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
3434
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
3535
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint;
36+
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
3637
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
3738
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
3839
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
@@ -56,6 +57,7 @@
5657
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
5758
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
5859

60+
import static org.assertj.core.api.Assertions.assertThat;
5961
import static org.hamcrest.Matchers.both;
6062
import static org.hamcrest.Matchers.hasKey;
6163
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
@@ -78,6 +80,16 @@ void close() {
7880
this.context.close();
7981
}
8082

83+
@Test
84+
void webMvcEndpointHandlerMappingIsConfiguredWithPathPatternParser() {
85+
this.context = new AnnotationConfigServletWebApplicationContext();
86+
this.context.register(DefaultConfiguration.class);
87+
this.context.setServletContext(new MockServletContext());
88+
this.context.refresh();
89+
WebMvcEndpointHandlerMapping handlerMapping = this.context.getBean(WebMvcEndpointHandlerMapping.class);
90+
assertThat(handlerMapping.getPatternParser()).isEqualTo(WebMvcAutoConfiguration.pathPatternParser);
91+
}
92+
8193
@Test
8294
void endpointsAreSecureByDefault() throws Exception {
8395
this.context = new AnnotationConfigServletWebApplicationContext();

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

+41-15
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
import org.springframework.web.servlet.handler.RequestMatchResult;
6868
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
6969
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
70-
import org.springframework.web.util.UrlPathHelper;
70+
import org.springframework.web.util.pattern.PathPatternParser;
7171

7272
/**
7373
* A custom {@link HandlerMapping} that makes {@link ExposableWebEndpoint web endpoints}
@@ -95,7 +95,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
9595
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, "handle",
9696
HttpServletRequest.class, Map.class);
9797

98-
private static final RequestMappingInfo.BuilderConfiguration builderConfig = getBuilderConfig();
98+
private RequestMappingInfo.BuilderConfiguration builderConfig = new RequestMappingInfo.BuilderConfiguration();
9999

100100
/**
101101
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
@@ -123,14 +123,48 @@ public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
123123
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
124124
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
125125
CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping) {
126+
this(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, shouldRegisterLinksMapping, null);
127+
}
128+
129+
/**
130+
* Creates a new {@code AbstractWebMvcEndpointHandlerMapping} that provides mappings
131+
* for the operations of the given endpoints.
132+
* @param endpointMapping the base mapping for all endpoints
133+
* @param endpoints the web endpoints
134+
* @param endpointMediaTypes media types consumed and produced by the endpoints
135+
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
136+
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
137+
* @param pathPatternParser the path pattern parser
138+
*/
139+
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
140+
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
141+
CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping,
142+
PathPatternParser pathPatternParser) {
126143
this.endpointMapping = endpointMapping;
127144
this.endpoints = endpoints;
128145
this.endpointMediaTypes = endpointMediaTypes;
129146
this.corsConfiguration = corsConfiguration;
130147
this.shouldRegisterLinksMapping = shouldRegisterLinksMapping;
148+
setPatternParser(pathPatternParser);
131149
setOrder(-100);
132150
}
133151

152+
@Override
153+
@SuppressWarnings("deprecation")
154+
public void afterPropertiesSet() {
155+
this.builderConfig = new RequestMappingInfo.BuilderConfiguration();
156+
if (getPatternParser() != null) {
157+
this.builderConfig.setPatternParser(getPatternParser());
158+
}
159+
else {
160+
this.builderConfig.setPathMatcher(null);
161+
this.builderConfig.setTrailingSlashMatch(true);
162+
this.builderConfig.setSuffixPatternMatch(false);
163+
164+
}
165+
super.afterPropertiesSet();
166+
}
167+
134168
@Override
135169
protected void initHandlerMethods() {
136170
for (ExposableWebEndpoint endpoint : this.endpoints) {
@@ -151,7 +185,8 @@ protected HandlerMethod createHandlerMethod(Object handler, Method method) {
151185

152186
@Override
153187
public RequestMatchResult match(HttpServletRequest request, String pattern) {
154-
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(builderConfig).build();
188+
Assert.isNull(getPatternParser(), "This HandlerMapping uses PathPatterns.");
189+
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.builderConfig).build();
155190
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
156191
if (matchingInfo == null) {
157192
return null;
@@ -161,15 +196,6 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) {
161196
return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
162197
}
163198

164-
@SuppressWarnings("deprecation")
165-
private static RequestMappingInfo.BuilderConfiguration getBuilderConfig() {
166-
RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
167-
config.setPathMatcher(null);
168-
config.setSuffixPatternMatch(false);
169-
config.setTrailingSlashMatch(true);
170-
return config;
171-
}
172-
173199
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
174200
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
175201
String path = predicate.getPath();
@@ -202,7 +228,7 @@ protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpo
202228
}
203229

204230
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
205-
return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path))
231+
return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path)).options(this.builderConfig)
206232
.methods(RequestMethod.valueOf(predicate.getHttpMethod().name()))
207233
.consumes(predicate.getConsumes().toArray(new String[0]))
208234
.produces(predicate.getProduces().toArray(new String[0])).build();
@@ -211,7 +237,7 @@ private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate
211237
private void registerLinksMapping() {
212238
RequestMappingInfo mapping = RequestMappingInfo.paths(this.endpointMapping.createSubPath(""))
213239
.methods(RequestMethod.GET).produces(this.endpointMediaTypes.getProduced().toArray(new String[0]))
214-
.options(builderConfig).build();
240+
.options(this.builderConfig).build();
215241
LinksHandler linksHandler = getLinksHandler();
216242
registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links",
217243
HttpServletRequest.class, HttpServletResponse.class));
@@ -335,7 +361,7 @@ private Map<String, Object> getArguments(HttpServletRequest request, Map<String,
335361
}
336362

337363
private Object getRemainingPathSegments(HttpServletRequest request) {
338-
String[] pathTokens = tokenize(request, UrlPathHelper.PATH_ATTRIBUTE, true);
364+
String[] pathTokens = tokenize(request, HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, true);
339365
String[] patternTokens = tokenize(request, HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, false);
340366
int numberOfRemainingPathSegments = pathTokens.length - patternTokens.length + 1;
341367
Assert.state(numberOfRemainingPathSegments >= 0, "Unable to extract remaining path segments");

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java

+22
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.web.bind.annotation.ResponseBody;
3232
import org.springframework.web.cors.CorsConfiguration;
3333
import org.springframework.web.servlet.HandlerMapping;
34+
import org.springframework.web.util.pattern.PathPatternParser;
3435

3536
/**
3637
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
@@ -62,6 +63,27 @@ public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<
6263
setOrder(-100);
6364
}
6465

66+
/**
67+
* Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings
68+
* for the given endpoints.
69+
* @param endpointMapping the base mapping for all endpoints
70+
* @param endpoints the web endpoints
71+
* @param endpointMediaTypes media types consumed and produced by the endpoints
72+
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
73+
* @param linksResolver resolver for determining links to available endpoints
74+
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
75+
* @param pathPatternParser the path pattern parser
76+
*/
77+
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<ExposableWebEndpoint> endpoints,
78+
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
79+
EndpointLinksResolver linksResolver, boolean shouldRegisterLinksMapping,
80+
PathPatternParser pathPatternParser) {
81+
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, shouldRegisterLinksMapping,
82+
pathPatternParser);
83+
this.linksResolver = linksResolver;
84+
setOrder(-100);
85+
}
86+
6587
@Override
6688
protected LinksHandler getLinksHandler() {
6789
return new WebMvcLinksHandler();

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java

+49-5
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@
5656
import org.springframework.web.cors.CorsConfiguration;
5757
import org.springframework.web.filter.OncePerRequestFilter;
5858
import org.springframework.web.servlet.handler.RequestMatchResult;
59+
import org.springframework.web.util.ServletRequestPathUtils;
60+
import org.springframework.web.util.pattern.PathPatternParser;
5961

6062
import static org.assertj.core.api.Assertions.assertThat;
63+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
6164

6265
/**
6366
* Integration tests for web endpoints exposed using Spring MVC.
@@ -104,24 +107,37 @@ void readOperationsThatReturnAResourceSupportRangeRequests() {
104107
});
105108
}
106109

110+
@Test
111+
void matchWhenPathPatternParserShouldThrowException() {
112+
assertThatIllegalArgumentException().isThrownBy(() -> getMatchResult("/spring/", true));
113+
}
114+
107115
@Test
108116
void matchWhenRequestHasTrailingSlashShouldNotBeNull() {
109-
assertThat(getMatchResult("/spring/")).isNotNull();
117+
assertThat(getMatchResult("/spring/", false)).isNotNull();
110118
}
111119

112120
@Test
113121
void matchWhenRequestHasSuffixShouldBeNull() {
114-
assertThat(getMatchResult("/spring.do")).isNull();
122+
assertThat(getMatchResult("/spring.do", false)).isNull();
115123
}
116124

117-
private RequestMatchResult getMatchResult(String servletPath) {
125+
private RequestMatchResult getMatchResult(String servletPath, boolean isPatternParser) {
118126
MockHttpServletRequest request = new MockHttpServletRequest();
119127
request.setServletPath(servletPath);
120-
AnnotationConfigServletWebServerApplicationContext context = createApplicationContext();
128+
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
129+
if (isPatternParser) {
130+
context.register(WebMvcConfiguration.class);
131+
}
132+
else {
133+
context.register(PathMatcherWebMvcConfiguration.class);
134+
}
121135
context.register(TestEndpointConfiguration.class);
122136
context.refresh();
123137
WebMvcEndpointHandlerMapping bean = context.getBean(WebMvcEndpointHandlerMapping.class);
124138
try {
139+
// Setup request attributes
140+
ServletRequestPathUtils.parseAndCache(request);
125141
// Trigger initLookupPath
126142
bean.getHandler(request);
127143
}
@@ -156,7 +172,35 @@ WebMvcEndpointHandlerMapping webEndpointHandlerMapping(Environment environment,
156172
String endpointPath = environment.getProperty("endpointPath");
157173
return new WebMvcEndpointHandlerMapping(new EndpointMapping(endpointPath),
158174
endpointDiscoverer.getEndpoints(), endpointMediaTypes, corsConfiguration,
159-
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()), StringUtils.hasText(endpointPath));
175+
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()), StringUtils.hasText(endpointPath),
176+
new PathPatternParser());
177+
}
178+
179+
}
180+
181+
@Configuration(proxyBeanMethods = false)
182+
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
183+
ServletWebServerFactoryAutoConfiguration.class, WebMvcAutoConfiguration.class,
184+
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class })
185+
static class PathMatcherWebMvcConfiguration {
186+
187+
@Bean
188+
TomcatServletWebServerFactory tomcat() {
189+
return new TomcatServletWebServerFactory(0);
190+
}
191+
192+
@Bean
193+
WebMvcEndpointHandlerMapping webEndpointHandlerMapping(Environment environment,
194+
WebEndpointDiscoverer endpointDiscoverer, EndpointMediaTypes endpointMediaTypes) {
195+
CorsConfiguration corsConfiguration = new CorsConfiguration();
196+
corsConfiguration.setAllowedOrigins(Arrays.asList("https://example.com"));
197+
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
198+
String endpointPath = environment.getProperty("endpointPath");
199+
WebMvcEndpointHandlerMapping handlerMapping = new WebMvcEndpointHandlerMapping(
200+
new EndpointMapping(endpointPath), endpointDiscoverer.getEndpoints(), endpointMediaTypes,
201+
corsConfiguration, new EndpointLinksResolver(endpointDiscoverer.getEndpoints()),
202+
StringUtils.hasText(endpointPath));
203+
return handlerMapping;
160204
}
161205

162206
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ public class WebMvcAutoConfiguration {
158158
*/
159159
public static final String DEFAULT_SUFFIX = "";
160160

161+
/**
162+
* Instance of {@link PathPatternParser} shared across MVC and actuator configuration.
163+
*/
164+
public static final PathPatternParser pathPatternParser = new PathPatternParser();
165+
161166
private static final String SERVLET_LOCATION = "/";
162167

163168
@Bean
@@ -246,7 +251,7 @@ public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
246251
public void configurePathMatch(PathMatchConfigurer configurer) {
247252
if (this.mvcProperties.getPathmatch()
248253
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
249-
configurer.setPatternParser(new PathPatternParser());
254+
configurer.setPatternParser(pathPatternParser);
250255
}
251256
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
252257
configurer.setUseRegisteredSuffixPatternMatch(

0 commit comments

Comments
 (0)