• spring 中的路径匹配


    spring 中,不管是注解使用时配置的基础包扫描路径,还是在 spring MVC 中根据路径匹配到具体的 handler,都会使用到路径匹配,今天我们来看下 spring 中的路径匹配到底是如何实现的。

    glob pattern 语法

    spring 借鉴了一种称之为 glob pattern 的语法,这是一种常用的匹配文件路径的模式语法,通过简单的规则来匹配文件名、路径名等,实现文件的查找、筛选等操作。常用在 Unix/Linux 系统的 shell 编程中。

    下面我们来详细看下语法:

    在 glob pattern 常用的特殊字符只有三个,'?'、'*'、'{',含义如下:

    • ?  匹配一个字符
    • *  匹配零个或多个字符
    • {xxx, xxx} 匹配 {} 中任意一组字符 

    spring 中的实现

    在 spring 中定义了基于字符串的路径匹配的接口,org.springframework.util.PathMatcher,目前只有一个实现类,org.springframework.util.AntPathMatcher,所以接下来我们所描述的路径匹配实现都是基于 AntPathMatcher。 

    在 AntPathMatcher 中,匹配规则如下:

    • ? 匹配单个任意字符
    • *  匹配零个或多个任意字符
    • ** 匹配单个或多个路径中的文件夹,即匹配当前包及其子包
    • {spring:[a-z]+} 匹配regexp [a-z]+作为名为"spring"的路径变量

    在 AntPathMatcher 中定义了是否采用此种匹配规则的方法 AntPathMatcher#isPattern,其实就是判断路径中是否存在规则中出现的三个特殊字符。

    1. @Override
    2. public boolean isPattern(@Nullable String path) {
    3. if (path == null) {
    4. return false;
    5. }
    6. boolean uriVar = false;
    7. for (int i = 0; i < path.length(); i++) {
    8. char c = path.charAt(i);
    9. if (c == '*' || c == '?') {
    10. return true;
    11. }
    12. if (c == '{') {
    13. uriVar = true;
    14. continue;
    15. }
    16. if (c == '}' && uriVar) {
    17. return true;
    18. }
    19. }
    20. return false;
    21. }

    在 AntPathMatcher#doMatch 中定义了匹配的算法逻辑,而将真正的字符串匹配委托给了内部类AntPathStringMatcher。

    下面来看下 AntPathStringMatcher 的创建

    1. private boolean matchStrings(String pattern, String str,
    2. @Nullable Map uriTemplateVariables) {
    3. return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);
    4. }
    5. // 获取 pattern 对应的匹配器
    6. protected AntPathStringMatcher getStringMatcher(String pattern) {
    7. AntPathStringMatcher matcher = null;
    8. Boolean cachePatterns = this.cachePatterns;
    9. if (cachePatterns == null || cachePatterns.booleanValue()) {
    10. matcher = this.stringMatcherCache.get(pattern);
    11. }
    12. if (matcher == null) {
    13. // AntPathMatcher 内部类
    14. matcher = new AntPathStringMatcher(pattern, this.caseSensitive);
    15. if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) {
    16. // 超过阈值,关闭缓存,上限 65536
    17. deactivatePatternCache();
    18. return matcher;
    19. }
    20. if (cachePatterns == null || cachePatterns.booleanValue()) {
    21. // 加入缓存
    22. this.stringMatcherCache.put(pattern, matcher);
    23. }
    24. }
    25. return matcher;
    26. }

    可以看到,在字符串路径匹配时,先去获取字符串路径匹配器,此时将一个个 pattern 片段作为 key,去匹配器缓存获取一遍,获取不到,才进行创建。

    在路径匹配时,传入的路径都是绝对路径,所以会存在很多相同的片段,此处应用缓存,也是为了提高匹配器的复用,减少对象的创建。

    1. private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
    2. // * 0个或多个
    3. // ? 1个
    4. // {} url路径 /user/{user}
    5. public AntPathStringMatcher(String pattern, boolean caseSensitive) {
    6. this.rawPattern = pattern;
    7. this.caseSensitive = caseSensitive;
    8. StringBuilder patternBuilder = new StringBuilder();
    9. // 采用 glob pattern 语法
    10. Matcher matcher = GLOB_PATTERN.matcher(pattern);
    11. int end = 0;
    12. // 将 glob pattern 语法转化为 java 正则语法
    13. while (matcher.find()) {
    14. patternBuilder.append(quote(pattern, end, matcher.start()));
    15. String match = matcher.group();
    16. if ("?".equals(match)) {
    17. patternBuilder.append('.');
    18. }
    19. else if ("*".equals(match)) {
    20. patternBuilder.append(".*");
    21. }
    22. // {}
    23. else if (match.startsWith("{") && match.endsWith("}")) {
    24. int colonIdx = match.indexOf(':');
    25. if (colonIdx == -1) {
    26. patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
    27. this.variableNames.add(matcher.group(1));
    28. }
    29. else {
    30. // ':' 之前的作为 variableName, 之后的部分作为 variablePattern
    31. String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
    32. patternBuilder.append('(');
    33. patternBuilder.append(variablePattern);
    34. patternBuilder.append(')');
    35. String variableName = match.substring(1, colonIdx);
    36. this.variableNames.add(variableName);
    37. }
    38. }
    39. end = matcher.end();
    40. }
    41. // 没有 glob pattern 语法,完整字符串匹配
    42. if (end == 0) {
    43. this.exactMatch = true;
    44. this.pattern = null;
    45. }
    46. else {
    47. this.exactMatch = false;
    48. patternBuilder.append(quote(pattern, end, pattern.length()));
    49. this.pattern = Pattern.compile(patternBuilder.toString(),
    50. Pattern.DOTALL | (this.caseSensitive ? 0 : Pattern.CASE_INSENSITIVE));
    51. }
    52. }

    由于 glob pattern 匹配语法与 java 正则语法有区别,此处将 glob pattern 语法做了转化,转化为 java 的正则语法,再进行了编译,赋值给 AntPathStringMatcher.pattern 属性。

    此处的 quote 方法是采用了 java Pattern#quote 中的处理,对不需要匹配的字符串引用起来,不看做正则表达式,比如:".class" 引用完之后,变成 "\Q.class\E",这样字符串中的字符 '.' 就不会看作正则中的特殊字符。

    循环时,采用 Matcher#find 方法,此方法会找出参数 pattern 中的下一个子序列与 GLOB_PATTERN,进行匹配,比如:"*.class",执行 Matcher#find 方法后,再执行 Matcher#group 会捕获到 "*",此时经过转化变成 ".*",最后拼接,重新编译去匹配的正则表达式就是:".*\Q.class\E"。

    1. // org.springframework.util.AntPathMatcher$AntPathStringMatcher
    2. public boolean matchStrings(String str, @Nullable Map uriTemplateVariables) {
    3. // 精确匹配
    4. if (this.exactMatch) {
    5. return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str);
    6. }
    7. // 正则匹配
    8. else if (this.pattern != null) {
    9. Matcher matcher = this.pattern.matcher(str);
    10. if (matcher.matches()) {
    11. if (uriTemplateVariables != null) {
    12. if (this.variableNames.size() != matcher.groupCount()) {
    13. throw ...
    14. }
    15. for (int i = 1; i <= matcher.groupCount(); i++) {
    16. String name = this.variableNames.get(i - 1);
    17. if (name.startsWith("*")) {
    18. throw ...
    19. }
    20. String value = matcher.group(i);
    21. uriTemplateVariables.put(name, value);
    22. }
    23. }
    24. return true;
    25. }
    26. }
    27. return false;
    28. }

    执行完准备工作,待到字符串具体匹配时,就很简单了,是精确匹配,默认大小写敏感,此时调用 equals 方法判断两个字符串是否相等,非精确匹配,此时已经将 glob pattern 转化为 java 的正则匹配,直接调用 Matcher#matches 方法即可。

    下面来看下 AntPathMatcher#doMatch 的匹配逻辑:

    1. // AntPathMatcher
    2. @Override
    3. public boolean match(String pattern, String path) {
    4. return doMatch(pattern, path, true, null);
    5. }
    6. // 匹配
    7. protected boolean doMatch(String pattern, @Nullable String path, boolean fullMatch,
    8. @Nullable Map uriTemplateVariables) {
    9. // path 路径不存在或者和 pattern 的开头不一致,直接返回 false
    10. if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
    11. return false;
    12. }
    13. // 分割 pattern
    14. String[] pattDirs = tokenizePattern(pattern);
    15. // caseSensitive true 默认大小写敏感
    16. // isPotentialMatch 匹配到 pattDirs 中通配符就返回了
    17. if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) {
    18. return false;
    19. }
    20. // 分割 path
    21. String[] pathDirs = tokenizePath(path);
    22. int pattIdxStart = 0;
    23. int pattIdxEnd = pattDirs.length - 1;
    24. int pathIdxStart = 0;
    25. int pathIdxEnd = pathDirs.length - 1;
    26. // Match all elements up to the first **
    27. while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
    28. String pattDir = pattDirs[pattIdxStart];
    29. // pattDir 是 "**" 时跳出循环
    30. if ("**".equals(pattDir)) {
    31. break;
    32. }
    33. // 字符串匹配,不匹配直接返回 false
    34. if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
    35. return false;
    36. }
    37. // 对于匹配过的,都执行自增
    38. pattIdxStart++;
    39. pathIdxStart++;
    40. }
    41. // path 已经耗尽
    42. if (pathIdxStart > pathIdxEnd) {
    43. // Path is exhausted, only match if rest of pattern is * or **'s
    44. // pattern 也耗尽,判断 pattern 和 path 结尾是否都以 pathSeparator 结尾
    45. if (pattIdxStart > pattIdxEnd) {
    46. return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
    47. }
    48. // pattern 未耗尽,fullMatch 为 false 直接返回
    49. // fullMatch 传参为 true,此处并不会返回
    50. if (!fullMatch) {
    51. return true;
    52. }
    53. // path 耗尽,pattern 也走到了结尾,判断 pattern 末端是否为 "*",并且此时 path 以 pathSeparator 结尾
    54. if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
    55. return true;
    56. }
    57. // path 耗尽,pattern 还存在,只能是 "**",不为 "**" 直接返回 false,不匹配
    58. for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
    59. if (!pattDirs[i].equals("**")) {
    60. return false;
    61. }
    62. }
    63. return true;
    64. }
    65. // pattern 耗尽,直接返回 false,因为 path 耗尽的情况上面已经分析了,此处即 path 还存在
    66. else if (pattIdxStart > pattIdxEnd) {
    67. // String not exhausted, but pattern is. Failure.
    68. return false;
    69. }
    70. // fullMatch 为 false,最前面 pattern 遇到 "**" 跳出循环,path 和 pattern 都未耗尽,此时 pattern 为 "**",返回true
    71. // 执行时传参 fullMatch 为 true,所以此处判断直接跳过
    72. else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
    73. // Path start definitely matches due to "**" part in pattern.
    74. return true;
    75. }
    76. // path 和 pattern 都未耗尽,pattern 遇到了 "**",跳出了前面的 while 循环
    77. // up to last '**'
    78. while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
    79. // 取最后一个 pattern
    80. String pattDir = pattDirs[pattIdxEnd];
    81. // 为 "**" 跳出循环
    82. if (pattDir.equals("**")) {
    83. break;
    84. }
    85. // 末尾字符串匹配,不匹配直接返回 false
    86. if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
    87. return false;
    88. }
    89. // 对于匹配过的,都执行自减
    90. pattIdxEnd--;
    91. pathIdxEnd--;
    92. }
    93. // pattern 末尾和 path 一直能匹配上,但 path 的 idxEnd 已经小于 idxStart,说明 path 已耗尽
    94. //示例:pattern: "a/b/c/**/e/f/g",只有 e 和 f 都变为 **,才匹配
    95. // path: "a/b/c/g"
    96. if (pathIdxStart > pathIdxEnd) {
    97. // String is exhausted
    98. // 剩下的 pattern 只有全为 "**" 才能满足匹配,否则返回 false,不匹配
    99. for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
    100. if (!pattDirs[i].equals("**")) {
    101. return false;
    102. }
    103. }
    104. return true;
    105. }
    106. // pattern 短,path 长,只能是走到了 pattern 的最后一个 "**" 退出了循环,所以不存在 pattern 耗尽
    107. // 举例:
    108. // pattern: a/b/c/**/e/f/g/**/h/j pattIdxStart 3 pattIdxEnd 7
    109. // path: a/b/c/e/f/g/h/j pathIdxStart 3 pathIdxEnd 5
    110. // pattIdxStart != pattIdxEnd 才有中间部分需要匹配
    111. while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
    112. int patIdxTmp = -1;
    113. // pattIdxStart 为 "**",所以 i 从 pattidxStart + 1 开始,找下一个 "**"
    114. // pattern: a/b/c/**/e/f/g/**/**/h/j patIdxTmp 为 7
    115. for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
    116. if (pattDirs[i].equals("**")) {
    117. patIdxTmp = i;
    118. break;
    119. }
    120. }
    121. // "**/**" 这种情况,跳过一个,即 pattIdxStart 自增,接着执行下一次循环
    122. if (patIdxTmp == pattIdxStart + 1) {
    123. // '**/**' situation, so skip one
    124. pattIdxStart++;
    125. continue;
    126. }
    127. // Find the pattern between padIdxStart & padIdxTmp in str between
    128. // strIdxStart & strIdxEnd
    129. // 剩余 pattern 长度,即第一个和第二个,两个 "**" 之间 pattern 的个数
    130. int patLength = (patIdxTmp - pattIdxStart - 1);
    131. // 剩余 path 未匹配的个数
    132. int strLength = (pathIdxEnd - pathIdxStart + 1);
    133. int foundIdx = -1;
    134. // path 剩余长度 大于 pattern 长度,直接返回false
    135. // 只有 strLength >= pattern 长度,才会进入循环
    136. // strLength - patLength 即最大循环多少次,因为 "**" 可以匹配 path 中的子路径,即 path 子路径比 pattern 多出的部分
    137. // 举例:
    138. // pattern: a/b/c/**/e/f/g/**/h/j pattIdxStart 3 pattIdxEnd 7 patIdxTmp 7 patLength = 3
    139. // path: a/b/c/m/n/e/f/g/h/j pathIdxStart 3 pathIdxEnd 7 strLength = 5
    140. // pattern: a/b/c/**/e/f/g/**/**/h/j pattIdxStart 3 pattIdxEnd 8 patIdxTmp 7 patLength = 3
    141. // path: a/b/c/m/n/e/f/g/h/j pathIdxStart 3 pathIdxEnd 7 strLength = 5
    142. // pattern: a/b/c/**/e/f/**/g/**/**/h/j pattIdxStart 3 pattIdxEnd 9 patIdxTmp 6 patLength = 2
    143. // path: a/b/c/m/n/e/f/g/h/j pathIdxStart 3 pathIdxEnd 7 strLength = 5 foundIdx = 3 + 2 = 5
    144. // 匹配完更新 pattIdxStart 6 pathIdxStart = 5 + 2 = 7,接着再次进入循环,此时 patIdxTmp 8,计算出 patLength = 8-6-1=1,strLength = 7-7+1=1,即就剩一个未匹配了
    145. // 匹配完更新 foundIdx = 7+0=7,pattIdxStart = 8 pathIdxStart = 7 + 1 = 8,再次进入循环,发现 pathIdxStart > pathIdxEnd,path已耗尽,退出循环
    146. strLoop:
    147. for (int i = 0; i <= strLength - patLength; i++) {
    148. for (int j = 0; j < patLength; j++) {
    149. // pattIdxStart 此时为 "**", +1 即从下一个开始
    150. String subPat = pattDirs[pattIdxStart + j + 1];
    151. String subStr = pathDirs[pathIdxStart + i + j];
    152. // 不匹配,进行下一次循环
    153. // 因为 patternIdxStart 为 "**",可以匹配 path 中的子路径,所以即使字符串匹配返回 false,也不能认为不匹配,只需进行下一次循环即可
    154. if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
    155. continue strLoop;
    156. }
    157. }
    158. // 执行到此处,表明字符串匹配都成功了,退出循环
    159. // foundIdx = pathIdxStart + 子路径多出部分长度,即从这里开始与 pattern 匹配上了
    160. foundIdx = pathIdxStart + i;
    161. break;
    162. }
    163. // 循环完都匹配不上,直接返回 false
    164. if (foundIdx == -1) {
    165. return false;
    166. }
    167. // 更新 pattIdxStart 和 pathIdxStart
    168. // pattern 更新到最后一个 "**"
    169. pattIdxStart = patIdxTmp;
    170. // pathIdxStart 相当于等于 原pathIdxStart + 子路径部分长度 + pattern 部分长度
    171. pathIdxStart = foundIdx + patLength;
    172. }
    173. // 首尾部分都匹配完了,此时如果还剩下 pattern,只能为 "**",否则不匹配,返回 false
    174. // pattern: a/b/c/**/d/e/**/**/**/k/f/g
    175. // path: a/b/c/m/n/d/e/k/f/g
    176. // 执行到此处还有一种可能,就是 pattIdxStart == pattIdxEnd,并且为 "**",此时返回 true
    177. // pattern: a/b/c/**
    178. // path: a/b/c/e/f/g 此时也匹配
    179. for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
    180. if (!pattDirs[i].equals("**")) {
    181. return false;
    182. }
    183. }
    184. return true;
    185. }

    代码比较长,归纳总结其实现逻辑如下:

    1. pattern 和 path 从前匹配,匹配上了,更新 pattIdxStart 和 pathIdxStart,直到 pattern 遇见 **,退出循环
    2. 此时判断 pattern 和 path 是否耗尽,做出相应处理
    3. 都未耗尽,pattern 是遇到了第一个 ** 跳出了循环,此时从后向前匹配,匹配上了,更新 pattIdxEnd 和 pathIdxEnd,直到遇见 **,退出循环,此时还存在一种退出循环的可能,就是 path 耗尽了,pathIdxStart > pathIdxEnd。为什么不存在 pattern 耗尽的情况呢?pattern 耗尽,即 pattern 较短,path 较长,此时从上面已经得到了一个 **,只能是 pattern 遇见了 ** 退出循环,否则肯定是匹配不上的。举个例子,pattern:a/b/c/**/e/f,path:a/b/c/m/n/e/f
    4. 此时,pattIdxStart != pattIdxEnd,表示在首尾两个 ** 之间存在中间部分,继续循环,此时从 pattIdxStart + 1 开始,查找下一个 **,记录其索引 patIdxTmp,因为在首尾两个 ** 之间还可能存在 **,对于 **/** 这种情况,使 pattIdxStart 自增,进行下一次循环,直到pattIdxStart 和 patIdxTmp 之间存在其它部分,此时计算出 pattIdxStart 和 patIdxTmp 之间剩余多少需要匹配,由于 pattIdxStart 和 patIdxTmp 都对应 **,所以 patIdxTmp - pattIdxStart 之后还需要再减 1,即剔除 pattIdxStart。而对于 path,剩余未匹配的部分则为 pathIdxEnd - pathIdxStart,再加 1,包含 pathIdxStart。strLength - patLength,即最大可以存在几个子路径片段,因为 pattIdxStart 对应的 ** 可以匹配子路径,匹配完,计算一个 foundIdx,即 pathIdxStart + i,此处 i 表示从 pathIdxStart 开始,第几个索引对应的 path 片段开始与 pattern 片段匹配,i 从 0 开始。从这里开始与 pattern 片段匹配,之后更新 pattIdxStart = patIdxTmp,pathIdxStart = foundIdx + patLength,接着进行下一次循环
    5. 首尾,中间部分都匹配完了,一旦执行到了此处,存在的 pattern 剩余部分只能是 **

    至此,就完成了路径匹配的核心逻辑。

    下面的一些代码,在 AntPathMatcher#doMatch 中虽然也调用到了,权当了解。

    1. protected String[] tokenizePattern(String pattern) {
    2. String[] tokenized = null;
    3. Boolean cachePatterns = this.cachePatterns;
    4. if (cachePatterns == null || cachePatterns.booleanValue()) {
    5. // tokenizedPatternCache 缓存获取
    6. tokenized = this.tokenizedPatternCache.get(pattern);
    7. }
    8. if (tokenized == null) {
    9. tokenized = tokenizePath(pattern);
    10. if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
    11. // 超过阈值,关闭缓存
    12. deactivatePatternCache();
    13. return tokenized;
    14. }
    15. if (cachePatterns == null || cachePatterns.booleanValue()) {
    16. this.tokenizedPatternCache.put(pattern, tokenized);
    17. }
    18. }
    19. return tokenized;
    20. }
    21. protected String[] tokenizePath(String path) {
    22. // 使用 java.util.StringTokenizer
    23. return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
    24. }
    25. // StringUtils
    26. public static String[] tokenizeToStringArray(
    27. @Nullable String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
    28. if (str == null) {
    29. return EMPTY_STRING_ARRAY;
    30. }
    31. StringTokenizer st = new StringTokenizer(str, delimiters);
    32. List tokens = new ArrayList<>();
    33. while (st.hasMoreTokens()) {
    34. String token = st.nextToken();
    35. if (trimTokens) {
    36. token = token.trim();
    37. }
    38. // 是否忽略空 token
    39. if (!ignoreEmptyTokens || token.length() > 0) {
    40. tokens.add(token);
    41. }
    42. }
    43. return toStringArray(tokens);
    44. }
    45. // 可以认为基本上都是潜在的匹配
    46. private boolean isPotentialMatch(String path, String[] pattDirs) {
    47. if (!this.trimTokens) {
    48. // path 中当前字符索引,从 0 开始
    49. int pos = 0;
    50. for (String pattDir : pattDirs) {
    51. // 跳过路径分隔符
    52. int skipped = skipSeparator(path, pos, this.pathSeparator);
    53. pos += skipped;
    54. skipped = skipSegment(path, pos, pattDir);
    55. // skipped < pattDir.length() 表明 pattDir 中存在通配符,否则 skipped 长度应与 pattDir.length() 长度相等
    56. if (skipped < pattDir.length()) {
    57. return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0))));
    58. }
    59. pos += skipped;
    60. }
    61. }
    62. return true;
    63. }
    64. // 跳过分隔符
    65. private int skipSeparator(String path, int pos, String separator) {
    66. int skipped = 0;
    67. // 以分隔符开头,跳过分隔符
    68. while (path.startsWith(separator, pos + skipped)) {
    69. skipped += separator.length();
    70. }
    71. return skipped;
    72. }
    73. // abc*
    74. // 跳过片段
    75. private int skipSegment(String path, int pos, String prefix) {
    76. int skipped = 0;
    77. for (int i = 0; i < prefix.length(); i++) {
    78. char c = prefix.charAt(i);
    79. // 通配符字符,三个 '*'、'?'、'{'
    80. if (isWildcardChar(c)) {
    81. return skipped;
    82. }
    83. // path 中当前字符索引
    84. int currPos = pos + skipped;
    85. if (currPos >= path.length()) {
    86. return 0;
    87. }
    88. // 逐个字符比较,字符一致,skipped 自增
    89. if (c == path.charAt(currPos)) {
    90. skipped++;
    91. }
    92. }
    93. return skipped;
    94. }

    自 spring 5.0 开始,在 spring web 应用中还提供了另一个路径匹配处理类,PathPattern,语法与AntPathMatcher 类似。该解决方案专为 web 使用而设计,可以有效地处理编码和路径参数,并有效地匹配。

    PathPattern是web应用的推荐解决方案,也是Spring WebFlux的唯一选择。它从5.3版开始在Spring MVC中启用,从6.0版开始默认启用。而 AntPathMatcher 主要用于选择类路径、文件系统和其他位置上的资源。

  • 相关阅读:
    单调栈的相关应用
    vue+element 实现input批量查询条件
    基于 Hive 的 Flutter 文档类型存储
    Tailwindcss Layout布局相关样式及实战案例,5万字长文,附完整源码和效果截图
    【写在七夕浪浪漫时刻】Go中遇到http code 206和302的获取数据的解决方案
    Kubernetes (K8S) 1.24.3 For Ubuntu 安装
    Java中的File类的构造方法介绍使用、绝对路径和相对路径、File类的创建功能详细描述使用(上篇)
    C# EF框架增加和查询
    操作系统【OS】调度算法对比图
    MyBatis学习:动态SQL中<where>标签的使用
  • 原文地址:https://blog.csdn.net/zlk252620068/article/details/140357643