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
This commit is contained in:
parent
cdfcc23b6f
commit
e77ff3c991
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2015 the original author or authors.
|
* Copyright 2002-2016 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -85,7 +85,7 @@ public class AntPathMatcher implements PathMatcher {
|
||||||
|
|
||||||
private volatile Boolean cachePatterns;
|
private volatile Boolean cachePatterns;
|
||||||
|
|
||||||
private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<String, String[]>(256);
|
private final Map<String, PreprocessedPattern> tokenizedPatternCache = new ConcurrentHashMap<String, PreprocessedPattern>(256);
|
||||||
|
|
||||||
final Map<String, AntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, AntPathStringMatcher>(256);
|
final Map<String, AntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, AntPathStringMatcher>(256);
|
||||||
|
|
||||||
|
@ -187,7 +187,11 @@ public class AntPathMatcher implements PathMatcher {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] pattDirs = tokenizePattern(pattern);
|
PreprocessedPattern preprocessedPattern = tokenizePattern(pattern);
|
||||||
|
if (fullMatch && this.caseSensitive && preprocessedPattern.certainlyNotMatch(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String[] pattDirs = preprocessedPattern.tokenized;
|
||||||
String[] pathDirs = tokenizePath(path);
|
String[] pathDirs = tokenizePath(path);
|
||||||
|
|
||||||
int pattIdxStart = 0;
|
int pattIdxStart = 0;
|
||||||
|
@ -314,14 +318,14 @@ public class AntPathMatcher implements PathMatcher {
|
||||||
* @param pattern the pattern to tokenize
|
* @param pattern the pattern to tokenize
|
||||||
* @return the tokenized pattern parts
|
* @return the tokenized pattern parts
|
||||||
*/
|
*/
|
||||||
protected String[] tokenizePattern(String pattern) {
|
protected PreprocessedPattern tokenizePattern(String pattern) {
|
||||||
String[] tokenized = null;
|
PreprocessedPattern tokenized = null;
|
||||||
Boolean cachePatterns = this.cachePatterns;
|
Boolean cachePatterns = this.cachePatterns;
|
||||||
if (cachePatterns == null || cachePatterns.booleanValue()) {
|
if (cachePatterns == null || cachePatterns.booleanValue()) {
|
||||||
tokenized = this.tokenizedPatternCache.get(pattern);
|
tokenized = this.tokenizedPatternCache.get(pattern);
|
||||||
}
|
}
|
||||||
if (tokenized == null) {
|
if (tokenized == null) {
|
||||||
tokenized = tokenizePath(pattern);
|
tokenized = compiledPattern(pattern);
|
||||||
if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
|
if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
|
||||||
// Try to adapt to the runtime situation that we're encountering:
|
// Try to adapt to the runtime situation that we're encountering:
|
||||||
// There are obviously too many different patterns coming in here...
|
// There are obviously too many different patterns coming in here...
|
||||||
|
@ -345,6 +349,31 @@ public class AntPathMatcher implements PathMatcher {
|
||||||
return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
|
return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int firstSpecialCharIdx(int specialCharIdx, int prevFoundIdx) {
|
||||||
|
if (specialCharIdx != -1) {
|
||||||
|
return prevFoundIdx == -1 ? specialCharIdx : Math.min(prevFoundIdx, specialCharIdx);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return prevFoundIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreprocessedPattern compiledPattern(String pattern) {
|
||||||
|
String[] tokenized = tokenizePath(pattern);
|
||||||
|
int specialCharIdx = -1;
|
||||||
|
specialCharIdx = firstSpecialCharIdx(pattern.indexOf('*'), specialCharIdx);
|
||||||
|
specialCharIdx = firstSpecialCharIdx(pattern.indexOf('?'), specialCharIdx);
|
||||||
|
specialCharIdx = firstSpecialCharIdx(pattern.indexOf('{'), specialCharIdx);
|
||||||
|
final String prefix;
|
||||||
|
if (specialCharIdx != -1) {
|
||||||
|
prefix = pattern.substring(0, specialCharIdx);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
prefix = pattern;
|
||||||
|
}
|
||||||
|
return new PreprocessedPattern(tokenized, prefix.isEmpty() ? null : prefix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether or not a string matches against a pattern.
|
* Test whether or not a string matches against a pattern.
|
||||||
* @param pattern the pattern to match against (never {@code null})
|
* @param pattern the pattern to match against (never {@code null})
|
||||||
|
@ -847,4 +876,19 @@ public class AntPathMatcher implements PathMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class PreprocessedPattern {
|
||||||
|
private final String[] tokenized;
|
||||||
|
|
||||||
|
private final String prefix;
|
||||||
|
|
||||||
|
public PreprocessedPattern(String[] tokenized, String prefix) {
|
||||||
|
this.tokenized = tokenized;
|
||||||
|
this.prefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean certainlyNotMatch(String path) {
|
||||||
|
return prefix != null && !path.startsWith(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2015 the original author or authors.
|
* Copyright 2002-2016 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -418,8 +418,8 @@ public class AntPathMatcherTests {
|
||||||
assertEquals("/*.html", pathMatcher.combine("/**", "/*.html"));
|
assertEquals("/*.html", pathMatcher.combine("/**", "/*.html"));
|
||||||
assertEquals("/*.html", pathMatcher.combine("/*", "/*.html"));
|
assertEquals("/*.html", pathMatcher.combine("/*", "/*.html"));
|
||||||
assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html"));
|
assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html"));
|
||||||
assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858
|
assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858
|
||||||
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
|
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
|
||||||
assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062
|
assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062
|
||||||
assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); // SPR-10554
|
assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); // SPR-10554
|
||||||
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
|
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
|
||||||
|
@ -454,8 +454,8 @@ public class AntPathMatcherTests {
|
||||||
|
|
||||||
// SPR-10550
|
// SPR-10550
|
||||||
assertEquals(-1, comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**"));
|
assertEquals(-1, comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**"));
|
||||||
assertEquals(1, comparator.compare("/**","/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"));
|
assertEquals(1, comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"));
|
||||||
assertEquals(0, comparator.compare("/**","/**"));
|
assertEquals(0, comparator.compare("/**", "/**"));
|
||||||
|
|
||||||
assertEquals(-1, comparator.compare("/hotels/{hotel}", "/hotels/*"));
|
assertEquals(-1, comparator.compare("/hotels/{hotel}", "/hotels/*"));
|
||||||
assertEquals(1, comparator.compare("/hotels/*", "/hotels/{hotel}"));
|
assertEquals(1, comparator.compare("/hotels/*", "/hotels/{hotel}"));
|
||||||
|
@ -618,12 +618,44 @@ public class AntPathMatcherTests {
|
||||||
assertTrue(pathMatcher.stringMatcherCache.size() > 20);
|
assertTrue(pathMatcher.stringMatcherCache.size() > 20);
|
||||||
|
|
||||||
for (int i = 0; i < 65536; i++) {
|
for (int i = 0; i < 65536; i++) {
|
||||||
pathMatcher.match("test" + i, "test");
|
pathMatcher.match("test" + i, "test" + i);
|
||||||
}
|
}
|
||||||
// Cache keeps being alive due to the explicit cache setting
|
// Cache keeps being alive due to the explicit cache setting
|
||||||
assertTrue(pathMatcher.stringMatcherCache.size() > 65536);
|
assertTrue(pathMatcher.stringMatcherCache.size() > 65536);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preventCreatingStringMatchersIfPathDoesNotStartsWithPatternPrefix() {
|
||||||
|
pathMatcher.setCachePatterns(true);
|
||||||
|
assertEquals(0, pathMatcher.stringMatcherCache.size());
|
||||||
|
|
||||||
|
pathMatcher.match("test?", "test");
|
||||||
|
assertEquals(1, pathMatcher.stringMatcherCache.size());
|
||||||
|
|
||||||
|
pathMatcher.match("test?", "best");
|
||||||
|
pathMatcher.match("test/*", "view/test.jpg");
|
||||||
|
pathMatcher.match("test/**/test.jpg", "view/test.jpg");
|
||||||
|
pathMatcher.match("test/{name}.jpg", "view/test.jpg");
|
||||||
|
assertEquals(1, pathMatcher.stringMatcherCache.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void creatingStringMatchersIfPatternPrefixCannotDetermineIfPathMatch() {
|
||||||
|
pathMatcher.setCachePatterns(true);
|
||||||
|
assertEquals(0, pathMatcher.stringMatcherCache.size());
|
||||||
|
|
||||||
|
pathMatcher.match("test", "testian");
|
||||||
|
pathMatcher.match("test?", "testFf");
|
||||||
|
pathMatcher.match("test/*", "test/dir/name.jpg");
|
||||||
|
pathMatcher.match("test/{name}.jpg", "test/lorem.jpg");
|
||||||
|
pathMatcher.match("bla/**/test.jpg", "bla/test.jpg");
|
||||||
|
pathMatcher.match("**/{name}.jpg", "test/lorem.jpg");
|
||||||
|
pathMatcher.match("/**/{name}.jpg", "/test/lorem.jpg");
|
||||||
|
pathMatcher.match("/*/dir/{name}.jpg", "/*/dir/lorem.jpg");
|
||||||
|
|
||||||
|
assertEquals(7, pathMatcher.stringMatcherCache.size());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void cachePatternsSetToFalse() {
|
public void cachePatternsSetToFalse() {
|
||||||
pathMatcher.setCachePatterns(false);
|
pathMatcher.setCachePatterns(false);
|
||||||
|
|
Loading…
Reference in New Issue