Skip to content

Commit 3261542

Browse files
committed
Refined caching of AntPathStringMatcher per pattern
Introduced a "setCachePatterns(boolean)" method for explicit configuration, a default turnoff threshold at 65536 entries (at which point we're deciding that caching isn't worthwhile because patterns are unlikely to be reoccurring often enough), and an "AntPathStringMatcher getStringMatcher(String pattern)" template method. Issue: SPR-10803
1 parent 62157fe commit 3261542

File tree

2 files changed

+205
-115
lines changed

2 files changed

+205
-115
lines changed

spring-core/src/main/java/org/springframework/util/AntPathMatcher.java

Lines changed: 168 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,56 @@
5050
*/
5151
public class AntPathMatcher implements PathMatcher {
5252

53+
private static final int CACHE_TURNOFF_THRESHOLD = 65536;
54+
5355
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}");
5456

5557
/** Default path separator: "/" */
5658
public static final String DEFAULT_PATH_SEPARATOR = "/";
5759

58-
private String pathSeparator = DEFAULT_PATH_SEPARATOR;
5960

60-
private final Map<String, AntPathStringMatcher> stringMatcherCache =
61-
new ConcurrentHashMap<String, AntPathStringMatcher>(256);
61+
private String pathSeparator = DEFAULT_PATH_SEPARATOR;
6262

6363
private boolean trimTokens = true;
6464

65+
private volatile Boolean cachePatterns;
66+
67+
final Map<String, AntPathStringMatcher> stringMatcherCache =
68+
new ConcurrentHashMap<String, AntPathStringMatcher>(256);
69+
6570

66-
/** Set the path separator to use for pattern parsing. Default is "/", as in Ant. */
71+
/**
72+
* Set the path separator to use for pattern parsing.
73+
* Default is "/", as in Ant.
74+
*/
6775
public void setPathSeparator(String pathSeparator) {
6876
this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
6977
}
7078

71-
/** Whether to trim tokenized paths and patterns. */
79+
/**
80+
* Specify whether to trim tokenized paths and patterns.
81+
* Default is {@code true}.
82+
*/
7283
public void setTrimTokens(boolean trimTokens) {
7384
this.trimTokens = trimTokens;
7485
}
7586

87+
/**
88+
* Specify whether to cache parsed pattern metadata for patterns passed
89+
* into this matcher's {@link #match} method. A value of {@code true}
90+
* activates an unlimited pattern cache; a value of {@code false} turns
91+
* the pattern cache off completely.
92+
* <p>Default is for the cache to be on, but with the variant to automatically
93+
* turn it off when encountering too many patterns to cache at runtime
94+
* (the threshold is 65536), assuming that arbitrary permutations of patterns
95+
* are coming in, with little chance for encountering a reoccurring pattern.
96+
* @see #getStringMatcher(String)
97+
*/
98+
public void setCachePatterns(boolean cachePatterns) {
99+
this.cachePatterns = cachePatterns;
100+
}
101+
102+
76103
@Override
77104
public boolean isPattern(String path) {
78105
return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
@@ -88,7 +115,6 @@ public boolean matchStart(String pattern, String path) {
88115
return doMatch(pattern, path, false, null);
89116
}
90117

91-
92118
/**
93119
* Actually match the given {@code path} against the given {@code pattern}.
94120
* @param pattern the pattern to match against
@@ -97,9 +123,7 @@ public boolean matchStart(String pattern, String path) {
97123
* as far as the given base path goes is sufficient)
98124
* @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't
99125
*/
100-
protected boolean doMatch(String pattern, String path, boolean fullMatch,
101-
Map<String, String> uriTemplateVariables) {
102-
126+
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {
103127
if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
104128
return false;
105129
}
@@ -114,11 +138,11 @@ protected boolean doMatch(String pattern, String path, boolean fullMatch,
114138

115139
// Match all elements up to the first **
116140
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
117-
String patDir = pattDirs[pattIdxStart];
118-
if ("**".equals(patDir)) {
141+
String pattDir = pattDirs[pattIdxStart];
142+
if ("**".equals(pattDir)) {
119143
break;
120144
}
121-
if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
145+
if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
122146
return false;
123147
}
124148
pattIdxStart++;
@@ -155,11 +179,11 @@ else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
155179

156180
// up to last '**'
157181
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
158-
String patDir = pattDirs[pattIdxEnd];
159-
if (patDir.equals("**")) {
182+
String pattDir = pattDirs[pattIdxEnd];
183+
if (pattDir.equals("**")) {
160184
break;
161185
}
162-
if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
186+
if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
163187
return false;
164188
}
165189
pattIdxEnd--;
@@ -225,20 +249,49 @@ else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
225249
}
226250

227251
/**
228-
* Tests whether or not a string matches against a pattern. The pattern may contain two special characters:
229-
* <br>'*' means zero or more characters
230-
* <br>'?' means one and only one character
231-
* @param pattern pattern to match against. Must not be {@code null}.
232-
* @param str string which must be matched against the pattern. Must not be {@code null}.
233-
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise.
252+
* Tests whether or not a string matches against a pattern.
253+
* @param pattern the pattern to match against (never {@code null})
254+
* @param str the String which must be matched against the pattern (never {@code null})
255+
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise
234256
*/
235257
private boolean matchStrings(String pattern, String str, Map<String, String> uriTemplateVariables) {
236-
AntPathStringMatcher matcher = this.stringMatcherCache.get(pattern);
258+
return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);
259+
}
260+
261+
/**
262+
* Build or retrieve an {@link AntPathStringMatcher} for the given pattern.
263+
* <p>The default implementation checks this AntPathMatcher's internal cache
264+
* (see {@link #setCachePatterns}, creating a new AntPathStringMatcher instance
265+
* through {@link AntPathStringMatcher#AntPathStringMatcher(String)} if none found.
266+
* When encountering too many patterns to cache at runtime (the threshold is 65536),
267+
* it turns the default cache off, assuming that arbitrary permutations of patterns
268+
* are coming in, with little chance for encountering a reoccurring pattern.
269+
* <p>This method may get overridden to implement a custom cache strategy.
270+
* @param pattern the pattern to match against (never {@code null})
271+
* @return a corresponding AntPathStringMatcher (never {@code null})
272+
* @see #setCachePatterns
273+
*/
274+
protected AntPathStringMatcher getStringMatcher(String pattern) {
275+
AntPathStringMatcher matcher = null;
276+
Boolean cachePatterns = this.cachePatterns;
277+
if (cachePatterns == null || cachePatterns.booleanValue()) {
278+
matcher = this.stringMatcherCache.get(pattern);
279+
}
237280
if (matcher == null) {
238281
matcher = new AntPathStringMatcher(pattern);
239-
this.stringMatcherCache.put(pattern, matcher);
282+
if (cachePatterns == null && this.stringMatcherCache.size() == CACHE_TURNOFF_THRESHOLD) {
283+
// Try to adapt to the runtime situation that we're encountering:
284+
// There are obviously too many different paths coming in here...
285+
// So let's turn off the cache since the patterns are unlikely to be reoccurring.
286+
this.cachePatterns = false;
287+
this.stringMatcherCache.clear();
288+
return matcher;
289+
}
290+
if (cachePatterns == null || cachePatterns.booleanValue()) {
291+
this.stringMatcherCache.put(pattern, matcher);
292+
}
240293
}
241-
return matcher.matchStrings(str, uriTemplateVariables);
294+
return matcher;
242295
}
243296

244297
/**
@@ -381,11 +434,99 @@ public Comparator<String> getPatternComparator(String path) {
381434
}
382435

383436

384-
private static class AntPatternComparator implements Comparator<String> {
437+
/**
438+
* Tests whether or not a string matches against a pattern via a {@link Pattern}.
439+
* <p>The pattern may contain special characters: '*' means zero or more characters; '?' means one and
440+
* only one character; '{' and '}' indicate a URI template pattern. For example <tt>/users/{user}</tt>.
441+
*/
442+
protected static class AntPathStringMatcher {
443+
444+
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
445+
446+
private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
447+
448+
private final Pattern pattern;
449+
450+
private final List<String> variableNames = new LinkedList<String>();
451+
452+
public AntPathStringMatcher(String pattern) {
453+
StringBuilder patternBuilder = new StringBuilder();
454+
Matcher m = GLOB_PATTERN.matcher(pattern);
455+
int end = 0;
456+
while (m.find()) {
457+
patternBuilder.append(quote(pattern, end, m.start()));
458+
String match = m.group();
459+
if ("?".equals(match)) {
460+
patternBuilder.append('.');
461+
}
462+
else if ("*".equals(match)) {
463+
patternBuilder.append(".*");
464+
}
465+
else if (match.startsWith("{") && match.endsWith("}")) {
466+
int colonIdx = match.indexOf(':');
467+
if (colonIdx == -1) {
468+
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
469+
this.variableNames.add(m.group(1));
470+
}
471+
else {
472+
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
473+
patternBuilder.append('(');
474+
patternBuilder.append(variablePattern);
475+
patternBuilder.append(')');
476+
String variableName = match.substring(1, colonIdx);
477+
this.variableNames.add(variableName);
478+
}
479+
}
480+
end = m.end();
481+
}
482+
patternBuilder.append(quote(pattern, end, pattern.length()));
483+
this.pattern = Pattern.compile(patternBuilder.toString());
484+
}
485+
486+
private String quote(String s, int start, int end) {
487+
if (start == end) {
488+
return "";
489+
}
490+
return Pattern.quote(s.substring(start, end));
491+
}
492+
493+
/**
494+
* Main entry point.
495+
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise.
496+
*/
497+
public boolean matchStrings(String str, Map<String, String> uriTemplateVariables) {
498+
Matcher matcher = this.pattern.matcher(str);
499+
if (matcher.matches()) {
500+
if (uriTemplateVariables != null) {
501+
// SPR-8455
502+
Assert.isTrue(this.variableNames.size() == matcher.groupCount(),
503+
"The number of capturing groups in the pattern segment " + this.pattern +
504+
" does not match the number of URI template variables it defines, which can occur if " +
505+
" capturing groups are used in a URI template regex. Use non-capturing groups instead.");
506+
for (int i = 1; i <= matcher.groupCount(); i++) {
507+
String name = this.variableNames.get(i - 1);
508+
String value = matcher.group(i);
509+
uriTemplateVariables.put(name, value);
510+
}
511+
}
512+
return true;
513+
}
514+
else {
515+
return false;
516+
}
517+
}
518+
}
519+
520+
521+
/**
522+
* The default {@link Comparator} implementation returned by
523+
* {@link #getPatternComparator(String)}.
524+
*/
525+
protected static class AntPatternComparator implements Comparator<String> {
385526

386527
private final String path;
387528

388-
private AntPatternComparator(String path) {
529+
public AntPatternComparator(String path) {
389530
this.path = path;
390531
}
391532

@@ -465,92 +606,7 @@ private int getWildCardCount(String pattern) {
465606
* Returns the length of the given pattern, where template variables are considered to be 1 long.
466607
*/
467608
private int getPatternLength(String pattern) {
468-
Matcher m = VARIABLE_PATTERN.matcher(pattern);
469-
return m.replaceAll("#").length();
470-
}
471-
}
472-
473-
474-
/**
475-
* Tests whether or not a string matches against a pattern via a {@link Pattern}.
476-
* <p>The pattern may contain special characters: '*' means zero or more characters; '?' means one and
477-
* only one character; '{' and '}' indicate a URI template pattern. For example <tt>/users/{user}</tt>.
478-
*/
479-
private static class AntPathStringMatcher {
480-
481-
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
482-
483-
private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
484-
485-
private final Pattern pattern;
486-
487-
private final List<String> variableNames = new LinkedList<String>();
488-
489-
public AntPathStringMatcher(String pattern) {
490-
StringBuilder patternBuilder = new StringBuilder();
491-
Matcher m = GLOB_PATTERN.matcher(pattern);
492-
int end = 0;
493-
while (m.find()) {
494-
patternBuilder.append(quote(pattern, end, m.start()));
495-
String match = m.group();
496-
if ("?".equals(match)) {
497-
patternBuilder.append('.');
498-
}
499-
else if ("*".equals(match)) {
500-
patternBuilder.append(".*");
501-
}
502-
else if (match.startsWith("{") && match.endsWith("}")) {
503-
int colonIdx = match.indexOf(':');
504-
if (colonIdx == -1) {
505-
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
506-
this.variableNames.add(m.group(1));
507-
}
508-
else {
509-
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
510-
patternBuilder.append('(');
511-
patternBuilder.append(variablePattern);
512-
patternBuilder.append(')');
513-
String variableName = match.substring(1, colonIdx);
514-
this.variableNames.add(variableName);
515-
}
516-
}
517-
end = m.end();
518-
}
519-
patternBuilder.append(quote(pattern, end, pattern.length()));
520-
this.pattern = Pattern.compile(patternBuilder.toString());
521-
}
522-
523-
private String quote(String s, int start, int end) {
524-
if (start == end) {
525-
return "";
526-
}
527-
return Pattern.quote(s.substring(start, end));
528-
}
529-
530-
/**
531-
* Main entry point.
532-
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise.
533-
*/
534-
public boolean matchStrings(String str, Map<String, String> uriTemplateVariables) {
535-
Matcher matcher = this.pattern.matcher(str);
536-
if (matcher.matches()) {
537-
if (uriTemplateVariables != null) {
538-
// SPR-8455
539-
Assert.isTrue(this.variableNames.size() == matcher.groupCount(),
540-
"The number of capturing groups in the pattern segment " + this.pattern +
541-
" does not match the number of URI template variables it defines, which can occur if " +
542-
" capturing groups are used in a URI template regex. Use non-capturing groups instead.");
543-
for (int i = 1; i <= matcher.groupCount(); i++) {
544-
String name = this.variableNames.get(i - 1);
545-
String value = matcher.group(i);
546-
uriTemplateVariables.put(name, value);
547-
}
548-
}
549-
return true;
550-
}
551-
else {
552-
return false;
553-
}
609+
return VARIABLE_PATTERN.matcher(pattern).replaceAll("#").length();
554610
}
555611
}
556612

0 commit comments

Comments
 (0)