Skip to content

Commit 34607d9

Browse files
committed
PathPattern caching with HandlerMappingIntrospector
This commit evolves the solution from M1 by parsing and caching patterns with the target HandlerMapping's PathPatternParser. This makes it unnecessary for callers to be aware of pattern parsing. Closes gh-25312
1 parent 32ab596 commit 34607d9

File tree

4 files changed

+117
-27
lines changed

4 files changed

+117
-27
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Properties;
25+
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.stream.Collectors;
2527

2628
import javax.servlet.http.HttpServletRequest;
2729
import javax.servlet.http.HttpServletRequestWrapper;
@@ -76,6 +78,10 @@ public class HandlerMappingIntrospector
7678
@Nullable
7779
private List<HandlerMapping> handlerMappings;
7880

81+
@Nullable
82+
private Map<HandlerMapping, MatchableHandlerMapping> pathPatternMatchableHandlerMappings =
83+
new ConcurrentHashMap<>();
84+
7985

8086
/**
8187
* Constructor for use with {@link ApplicationContextAware}.
@@ -113,6 +119,7 @@ public void afterPropertiesSet() {
113119
if (this.handlerMappings == null) {
114120
Assert.notNull(this.applicationContext, "No ApplicationContext");
115121
this.handlerMappings = initHandlerMappings(this.applicationContext);
122+
this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings);
116123
}
117124
}
118125

@@ -130,14 +137,16 @@ public void afterPropertiesSet() {
130137
@Nullable
131138
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
132139
Assert.notNull(this.handlerMappings, "Handler mappings not initialized");
140+
Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized");
133141
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);
134142
for (HandlerMapping handlerMapping : this.handlerMappings) {
135143
Object handler = handlerMapping.getHandler(wrapper);
136144
if (handler == null) {
137145
continue;
138146
}
139147
if (handlerMapping instanceof MatchableHandlerMapping) {
140-
return ((MatchableHandlerMapping) handlerMapping);
148+
return this.pathPatternMatchableHandlerMappings.getOrDefault(
149+
handlerMapping, (MatchableHandlerMapping) handlerMapping);
141150
}
142151
throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping");
143152
}
@@ -212,6 +221,16 @@ private static List<HandlerMapping> initFallback(ApplicationContext applicationC
212221
return result;
213222
}
214223

224+
private static Map<HandlerMapping, MatchableHandlerMapping> initPathPatternMatchableHandlerMappings(
225+
List<HandlerMapping> mappings) {
226+
227+
return mappings.stream()
228+
.filter(mapping -> mapping instanceof MatchableHandlerMapping)
229+
.map(mapping -> (MatchableHandlerMapping) mapping)
230+
.filter(mapping -> mapping.getPatternParser() != null)
231+
.collect(Collectors.toMap(mapping -> mapping, PathPatternMatchableHandlerMapping::new));
232+
}
233+
215234

216235
/**
217236
* Request wrapper that ignores request attribute changes.

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@
1818

1919
import javax.servlet.http.HttpServletRequest;
2020

21-
import org.springframework.http.server.PathContainer;
2221
import org.springframework.lang.Nullable;
2322
import org.springframework.web.servlet.HandlerMapping;
24-
import org.springframework.web.util.ServletRequestPathUtils;
25-
import org.springframework.web.util.pattern.PathPattern;
2623
import org.springframework.web.util.pattern.PathPatternParser;
2724

2825
/**
@@ -46,21 +43,6 @@ default PathPatternParser getPatternParser() {
4643
return null;
4744
}
4845

49-
/**
50-
* Determine whether the request matches the given pattern. Use this method
51-
* when {@link #getPatternParser()} is not {@code null} which means that the
52-
* {@code HandlerMapping} has pre-parsed patterns enabled.
53-
* @param request the current request
54-
* @param pattern the pattern to match
55-
* @return the result from request matching, or {@code null} if none
56-
* @since 5.3
57-
*/
58-
@Nullable
59-
default RequestMatchResult match(HttpServletRequest request, PathPattern pattern) {
60-
PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication();
61-
return (pattern.matches(path) ? new RequestMatchResult(pattern, path) : null);
62-
}
63-
6446
/**
6547
* Determine whether the request matches the given pattern. Use this method
6648
* when {@link #getPatternParser()} returns {@code null} which means that the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.servlet.handler;
17+
18+
import java.util.Map;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
21+
import javax.servlet.http.HttpServletRequest;
22+
23+
import org.springframework.http.server.PathContainer;
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.Assert;
26+
import org.springframework.web.servlet.HandlerExecutionChain;
27+
import org.springframework.web.util.ServletRequestPathUtils;
28+
import org.springframework.web.util.pattern.PathPattern;
29+
import org.springframework.web.util.pattern.PathPatternParser;
30+
31+
/**
32+
* Wraps {@link MatchableHandlerMapping}s configured with a {@link PathPatternParser}
33+
* in order to parse patterns lazily and cache them for re-ues.
34+
*
35+
* @author Rossen Stoyanchev
36+
* @since 5.3
37+
*/
38+
class PathPatternMatchableHandlerMapping implements MatchableHandlerMapping {
39+
40+
private static final int MAX_PATTERNS = 1024;
41+
42+
43+
private final MatchableHandlerMapping delegate;
44+
45+
private final PathPatternParser parser;
46+
47+
private final Map<String, PathPattern> pathPatternCache = new ConcurrentHashMap<>();
48+
49+
50+
public PathPatternMatchableHandlerMapping(MatchableHandlerMapping delegate) {
51+
Assert.notNull(delegate, "Delegate MatchableHandlerMapping is required.");
52+
Assert.notNull(delegate.getPatternParser(), "PatternParser is required.");
53+
this.delegate = delegate;
54+
this.parser = delegate.getPatternParser();
55+
}
56+
57+
@Nullable
58+
@Override
59+
public RequestMatchResult match(HttpServletRequest request, String pattern) {
60+
PathPattern pathPattern = this.pathPatternCache.computeIfAbsent(pattern, value -> {
61+
Assert.isTrue(this.pathPatternCache.size() < MAX_PATTERNS, "Max size for pattern cache exceeded.");
62+
return this.parser.parse(pattern);
63+
});
64+
PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication();
65+
return (pathPattern.matches(path) ? new RequestMatchResult(pathPattern, path) : null);
66+
}
67+
68+
@Nullable
69+
@Override
70+
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
71+
return this.delegate.getHandler(request);
72+
}
73+
}

spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.servlet.handler;
1818

19+
import java.util.ArrayList;
1920
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.List;
@@ -44,6 +45,7 @@
4445
import org.springframework.web.util.ServletRequestPathUtils;
4546
import org.springframework.web.util.pattern.PathPattern;
4647
import org.springframework.web.util.pattern.PathPatternParser;
48+
import org.springframework.web.util.pattern.PatternParseException;
4749

4850
import static org.assertj.core.api.Assertions.assertThat;
4951
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -111,7 +113,7 @@ void defaultHandlerMappings() {
111113
@ValueSource(booleans = {true, false})
112114
void getMatchable(boolean usePathPatterns) throws Exception {
113115

114-
PathPatternParser parser = new PathPatternParser();
116+
TestPathPatternParser parser = new TestPathPatternParser();
115117

116118
GenericWebApplicationContext context = new GenericWebApplicationContext();
117119
context.registerBean("mapping", SimpleUrlHandlerMapping.class, () -> {
@@ -134,16 +136,14 @@ void getMatchable(boolean usePathPatterns) throws Exception {
134136
MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request);
135137

136138
assertThat(mapping).isNotNull();
137-
assertThat(mapping).isEqualTo(context.getBean("mapping"));
138139
assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull();
139140

140-
String pattern = "/p*/*";
141-
PathPattern pathPattern = parser.parse(pattern);
142-
assertThat(usePathPatterns ? mapping.match(request, pathPattern) : mapping.match(request, pattern)).isNotNull();
141+
assertThat(mapping.match(request, "/p*/*")).isNotNull();
142+
assertThat(mapping.match(request, "/b*/*")).isNull();
143143

144-
pattern = "/b*/*";
145-
pathPattern = parser.parse(pattern);
146-
assertThat(usePathPatterns ? mapping.match(request, pathPattern) : mapping.match(request, pattern)).isNull();
144+
if (usePathPatterns) {
145+
assertThat(parser.getParsedPatterns()).containsExactly("/path/*", "/p*/*", "/b*/*");
146+
}
147147
}
148148

149149
@Test
@@ -230,4 +230,20 @@ void handle() {
230230
}
231231
}
232232

233+
private static class TestPathPatternParser extends PathPatternParser {
234+
235+
private final List<String> parsedPatterns = new ArrayList<>();
236+
237+
238+
public List<String> getParsedPatterns() {
239+
return this.parsedPatterns;
240+
}
241+
242+
@Override
243+
public PathPattern parse(String pathPattern) throws PatternParseException {
244+
this.parsedPatterns.add(pathPattern);
245+
return super.parse(pathPattern);
246+
}
247+
}
248+
233249
}

0 commit comments

Comments
 (0)