Skip to content

Commit e77ff3c

Browse files
m5k10bclozel
authored andcommitted
Improve AntPathMatcher matching performance
This commit speeds up the AntPathMatcher implementation by pre-processing patterns and checking that candidates are likely matches if they start with the static prefix of the pattern. Those changes can result in a small performance penalty for positive matches, but with a significant boost for checking candidates that don't match. Overall, this tradeoff is acceptable since this feature is often used to select a few matching patterns in a much bigger list. This will lead to small but consistent performance improvements in Spring MVC when matching a given request with the available routes. Issue: SPR-13913
1 parent cdfcc23 commit e77ff3c

File tree

2 files changed

+88
-12
lines changed

2 files changed

+88
-12
lines changed

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -85,7 +85,7 @@ public class AntPathMatcher implements PathMatcher {
8585

8686
private volatile Boolean cachePatterns;
8787

88-
private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<String, String[]>(256);
88+
private final Map<String, PreprocessedPattern> tokenizedPatternCache = new ConcurrentHashMap<String, PreprocessedPattern>(256);
8989

9090
final Map<String, AntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, AntPathStringMatcher>(256);
9191

@@ -187,7 +187,11 @@ protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<St
187187
return false;
188188
}
189189

190-
String[] pattDirs = tokenizePattern(pattern);
190+
PreprocessedPattern preprocessedPattern = tokenizePattern(pattern);
191+
if (fullMatch && this.caseSensitive && preprocessedPattern.certainlyNotMatch(path)) {
192+
return false;
193+
}
194+
String[] pattDirs = preprocessedPattern.tokenized;
191195
String[] pathDirs = tokenizePath(path);
192196

193197
int pattIdxStart = 0;
@@ -314,14 +318,14 @@ else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
314318
* @param pattern the pattern to tokenize
315319
* @return the tokenized pattern parts
316320
*/
317-
protected String[] tokenizePattern(String pattern) {
318-
String[] tokenized = null;
321+
protected PreprocessedPattern tokenizePattern(String pattern) {
322+
PreprocessedPattern tokenized = null;
319323
Boolean cachePatterns = this.cachePatterns;
320324
if (cachePatterns == null || cachePatterns.booleanValue()) {
321325
tokenized = this.tokenizedPatternCache.get(pattern);
322326
}
323327
if (tokenized == null) {
324-
tokenized = tokenizePath(pattern);
328+
tokenized = compiledPattern(pattern);
325329
if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
326330
// Try to adapt to the runtime situation that we're encountering:
327331
// There are obviously too many different patterns coming in here...
@@ -345,6 +349,31 @@ protected String[] tokenizePath(String path) {
345349
return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
346350
}
347351

352+
private int firstSpecialCharIdx(int specialCharIdx, int prevFoundIdx) {
353+
if (specialCharIdx != -1) {
354+
return prevFoundIdx == -1 ? specialCharIdx : Math.min(prevFoundIdx, specialCharIdx);
355+
}
356+
else {
357+
return prevFoundIdx;
358+
}
359+
}
360+
361+
private PreprocessedPattern compiledPattern(String pattern) {
362+
String[] tokenized = tokenizePath(pattern);
363+
int specialCharIdx = -1;
364+
specialCharIdx = firstSpecialCharIdx(pattern.indexOf('*'), specialCharIdx);
365+
specialCharIdx = firstSpecialCharIdx(pattern.indexOf('?'), specialCharIdx);
366+
specialCharIdx = firstSpecialCharIdx(pattern.indexOf('{'), specialCharIdx);
367+
final String prefix;
368+
if (specialCharIdx != -1) {
369+
prefix = pattern.substring(0, specialCharIdx);
370+
}
371+
else {
372+
prefix = pattern;
373+
}
374+
return new PreprocessedPattern(tokenized, prefix.isEmpty() ? null : prefix);
375+
}
376+
348377
/**
349378
* Test whether or not a string matches against a pattern.
350379
* @param pattern the pattern to match against (never {@code null})
@@ -847,4 +876,19 @@ public String getEndsOnDoubleWildCard() {
847876
}
848877
}
849878

879+
private static class PreprocessedPattern {
880+
private final String[] tokenized;
881+
882+
private final String prefix;
883+
884+
public PreprocessedPattern(String[] tokenized, String prefix) {
885+
this.tokenized = tokenized;
886+
this.prefix = prefix;
887+
}
888+
889+
private boolean certainlyNotMatch(String path) {
890+
return prefix != null && !path.startsWith(prefix);
891+
}
892+
}
893+
850894
}

spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -418,8 +418,8 @@ public void combine() {
418418
assertEquals("/*.html", pathMatcher.combine("/**", "/*.html"));
419419
assertEquals("/*.html", pathMatcher.combine("/*", "/*.html"));
420420
assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html"));
421-
assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858
422-
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
421+
assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858
422+
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
423423
assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062
424424
assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); // SPR-10554
425425
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
@@ -454,8 +454,8 @@ public void patternComparator() {
454454

455455
// SPR-10550
456456
assertEquals(-1, comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**"));
457-
assertEquals(1, comparator.compare("/**","/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"));
458-
assertEquals(0, comparator.compare("/**","/**"));
457+
assertEquals(1, comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"));
458+
assertEquals(0, comparator.compare("/**", "/**"));
459459

460460
assertEquals(-1, comparator.compare("/hotels/{hotel}", "/hotels/*"));
461461
assertEquals(1, comparator.compare("/hotels/*", "/hotels/{hotel}"));
@@ -618,12 +618,44 @@ public void cachePatternsSetToTrue() {
618618
assertTrue(pathMatcher.stringMatcherCache.size() > 20);
619619

620620
for (int i = 0; i < 65536; i++) {
621-
pathMatcher.match("test" + i, "test");
621+
pathMatcher.match("test" + i, "test" + i);
622622
}
623623
// Cache keeps being alive due to the explicit cache setting
624624
assertTrue(pathMatcher.stringMatcherCache.size() > 65536);
625625
}
626626

627+
@Test
628+
public void preventCreatingStringMatchersIfPathDoesNotStartsWithPatternPrefix() {
629+
pathMatcher.setCachePatterns(true);
630+
assertEquals(0, pathMatcher.stringMatcherCache.size());
631+
632+
pathMatcher.match("test?", "test");
633+
assertEquals(1, pathMatcher.stringMatcherCache.size());
634+
635+
pathMatcher.match("test?", "best");
636+
pathMatcher.match("test/*", "view/test.jpg");
637+
pathMatcher.match("test/**/test.jpg", "view/test.jpg");
638+
pathMatcher.match("test/{name}.jpg", "view/test.jpg");
639+
assertEquals(1, pathMatcher.stringMatcherCache.size());
640+
}
641+
642+
@Test
643+
public void creatingStringMatchersIfPatternPrefixCannotDetermineIfPathMatch() {
644+
pathMatcher.setCachePatterns(true);
645+
assertEquals(0, pathMatcher.stringMatcherCache.size());
646+
647+
pathMatcher.match("test", "testian");
648+
pathMatcher.match("test?", "testFf");
649+
pathMatcher.match("test/*", "test/dir/name.jpg");
650+
pathMatcher.match("test/{name}.jpg", "test/lorem.jpg");
651+
pathMatcher.match("bla/**/test.jpg", "bla/test.jpg");
652+
pathMatcher.match("**/{name}.jpg", "test/lorem.jpg");
653+
pathMatcher.match("/**/{name}.jpg", "/test/lorem.jpg");
654+
pathMatcher.match("/*/dir/{name}.jpg", "/*/dir/lorem.jpg");
655+
656+
assertEquals(7, pathMatcher.stringMatcherCache.size());
657+
}
658+
627659
@Test
628660
public void cachePatternsSetToFalse() {
629661
pathMatcher.setCachePatterns(false);

0 commit comments

Comments
 (0)