Spring源码之AntPathMatcher二:matchStrings-getStringMatcher-AntPathStringMatcher
上一篇文章 Spring源码之AntPathMatcher的doMatch算法写了AntPathMatcher类的doMatch方法将模式串pattern和字符串path以“/”分隔成数组,然后通过对应的算法,将各个数组取到的值进行匹配,其中就用到了matchStrings,比如下面用matchStrings:
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { //从第一个元素同时开始,对应匹配
String pattDir = pattDirs[pattIdxStart];
if ("**".equals(pattDir)) {//遇到模式串中的**就跳过,因为**表示匹配0个或多个目录
break;
}
if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {//matchStrings其内部实现就是将Ant风格的模式串 pattDir 转为正则表达式然后去和 pathDirs[pathIdxStart] 做匹配,匹配不上返回false
return false;
}
pattIdxStart++;
pathIdxStart++;
}
其中的matchStrings的作用就是将含有Ant风格通配符的模式串 pattDir 转为正则表达式然后去和 pathDirs[pathIdxStart] 做匹配,
什么是Ant风格的通配符呢?其实就是下面四种:
通配符 | 含义 | 例子 |
---|---|---|
? | 匹配任何单字符 | /aa/b?.jsp表示所有aa目录下的bX.jsp文件,比如/aa/bc.jsp |
* | 匹配0或者任意数量的字符 | /aa/b*.jsp表示所有aa目录下的bXXX.jsp文件,比如/aa/bcd.jsp,或者/aa/b.jsp |
** | 匹配0或者更多的目录 | /aa/**/bb.jsp表示所有aa目录下所有含有bb.jsp文件的目录,比如/aa/dd/bb.jsp或者/aa/ee/dd/bb.jsp |
{variableName:[a-z]+} | 配置正则表达式 [a-z]+ 作为variableName的值 | /aa/{filename:\w+}.jsp}将匹配/aa/test.jsp,并将test分配给filename变量(这个是springMvc4.3的新特性) |
Ant通配符最长匹配原则:/aa/bb/cc/dd.jsp同时符合下面两种模式的情况:/aa/**/*.jsp和/aa/bb/cc/d? .jsp。这种情况按照最长匹配原则由/aa/bb/cc/d? .jsp匹配。
好了,继续。通过源码我们可以看到matchStrings其实调用了getStringMatcher方法:
private boolean matchStrings(String pattern, String str,
@Nullable Map<String, String> uriTemplateVariables) {
return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);//getStringMatcher其内部实现就是将Ant风格的模式串转为正则表达式然后去和参数str做匹配
}
getStringMatcher方法返回的其实是一个AntPathStringMatcher对象,然后调用AntPathStringMatcher.matchStrings方法返回最终结果true或false。
getStringMatcher方法源码如下:
protected AntPathStringMatcher getStringMatcher(String pattern) { //其内部实现就是将Ant风格的模式串转为正则表达式然后去和str做匹配
AntPathStringMatcher matcher = null;
Boolean cachePatterns = this.cachePatterns;
if (cachePatterns == null || cachePatterns.booleanValue()) {
matcher = this.stringMatcherCache.get(pattern);//从缓存中查找,是否为null
}
if (matcher == null) { //缓存中没有这个pattern就去创建一个
matcher = new AntPathStringMatcher(pattern, this.caseSensitive);
if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) {
// Try to adapt to the runtime situation that we're encountering:
// There are obviously too many different patterns coming in here...
// So let's turn off the cache since the patterns are unlikely to be reoccurring.
deactivatePatternCache();
return matcher;
}
if (cachePatterns == null || cachePatterns.booleanValue()) {
this.stringMatcherCache.put(pattern, matcher);//将自己新建的matcher加入缓存中,如果下次用到这个pattern,就不用新建了,增加效率
}
}
return matcher;
}
通过源码可以看出getStringMatcher先从缓存中查找是否已经有了这个pattern被转换了如果有就不去做剩下的操作了,直接用缓存中的,提高效率,
matcher = this.stringMatcherCache.get(pattern);//从缓存中查找,是否为null
如果没有就去创建一个,然后加入缓存中
matcher = new AntPathStringMatcher(pattern, this.caseSensitive);
。。。
his.stringMatcherCache.put(pattern, matcher);//将自己新建的matcher加入缓存中,如果下次用到这个pattern,就不用新建了,增加效率
我们可以看到new AntPathStringMatcher时传入了模式串和是否区分大小写的参数
new AntPathStringMatcher(pattern, this.caseSensitive)
下面看内部类AntPathStringMatcher,是怎么将Ant风格通配符转为正则表达式的并进行匹配的,其构造函数如下
public AntPathStringMatcher(String pattern, boolean caseSensitive) {
StringBuilder patternBuilder = new StringBuilder();
Matcher matcher = GLOB_PATTERN.matcher(pattern);//GLOB_PATTERN正则表达式和模式串pattern匹配,会得到一个matcher,matcher中包含一些方法,得到匹配的一些情况
int end = 0;
while (matcher.find()) {
patternBuilder.append(quote(pattern, end, matcher.start()));//在找到匹配的情况下将前面的字符串先放入patternBuilder中
String match = matcher.group();
if ("?".equals(match)) { //如果找到了?就转换为正则表达式中的'.'
patternBuilder.append('.');
}
else if ("*".equals(match)) { //如果找到了*就转换为正则表达式中的'.*'
patternBuilder.append(".*");
}
else if (match.startsWith("{") && match.endsWith("}")) { //如果是在{}中就做下处理
int colonIdx = match.indexOf(':');
if (colonIdx == -1) { //如果没有包含':'直接就加DEFAULT_VARIABLE_PATTERN:(.*)
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
this.variableNames.add(matcher.group(1));//matcher.group(1)表示GLOB_PATTERN中的分组捕获的第一个
}
else {//如果{}含有":",则将{}中的内容拆分下,
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);//取":"后面的字符串
patternBuilder.append('(');
patternBuilder.append(variablePattern);
patternBuilder.append(')');
String variableName = match.substring(1, colonIdx);//取变量名,比如常见的classpath:mapper/*.xml中的classpath
this.variableNames.add(variableName);
}
}
end = matcher.end();
}
patternBuilder.append(quote(pattern, end, pattern.length()));
this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) :
Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));//最终生成一个正则表达式,caseSensitive用来区分大小写的
}
这也是核心的地方,首先将传入的Ant风格的pattern模式串与定义的GLOB_PATTERN做一次匹配matcher:
Matcher matcher = GLOB_PATTERN.matcher(pattern);//GLOB_PATTERN正则表达式和模式串pattern匹配,会得到一个matcher,matcher中包含一些方法,得到匹配的一些情况
其中的GLOB_PATTERN就是我们编译的一个模式串对象
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}");//第一个"\"都是转义"\"的,()的内部是一个分组
将这个模式串与传参pattern进行matcher得到一个Matcher对象,这个对象包含匹配的一些情况,比如matcher.find()为true就说明这个模式字符串pattern和GLOB_PATTERN是匹配到的,再比如matcher.group()得到的是匹配的那个字符,例如“?”匹配到了,这个时候matcher.group()就是“?”。
那么继续,如果匹配到了,也就是matcher.find()为true。就先将匹配到的字符串之前的不是通配符的字符串放入建好的StringBuilder中:
StringBuilder patternBuilder = new StringBuilder();
。。。
patternBuilder.append(quote(pattern, end, matcher.start()));//在找到匹配的情况下,比如将?之前的字符串先放入patternBuilder中
这个patternBuilder,最终就是我们转为正则表达式的一个字符串。
好,继续,匹配到了之后呢,如果匹配到的字符String match = matcher.group()是“?”,就替换成正则表达式中对应的’.’:
if ("?".equals(match)) { //如果找到了?就转换为正则表达式中的'.'
patternBuilder.append('.');
}
如果是“*”就是替换成正则表达式中的".*"
else if ("*".equals(match)) { //如果找到了*就转换为正则表达式中的'.*'
patternBuilder.append(".*");
}
如果是花括号包住的,就先判断是不是带有“:”,如果没有就直接转换成正则表达式中的"(.*)",同时将变量名提取出来,
this.variableNames.add(matcher.group(1));//matcher.group(1)表示GLOB_PATTERN中的分组捕获的第一个
如果有“:”,比如{path:\w+},这个时候就需要通过“:”分隔开来,将“:”后面的正则表达式\w+放在()中,然后提取变量名
else {//如果{}含有":",则将{}中的内容拆分下,
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);//取":"后面的字符串
patternBuilder.append('(');
patternBuilder.append(variablePattern);
patternBuilder.append(')');
String variableName = match.substring(1, colonIdx);//提取变量名
this.variableNames.add(variableName);
}
最后,将模式串pattern尾部不是通配符的字符串放入patternBuilder中,然后最终生成一个正则表达式this.pattern
patternBuilder.append(quote(pattern, end, pattern.length()));//将模式串pattern尾部不是通配符的字符串放入patternBuilder中
this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) :
Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));//最终生成一个正则表达式,caseSensitive用来区分大小写的
其中quote方法是用来将不是通配符的字符串做为字面常量来处理,以提高效率,比如传入的模式串为aa{path}cc}其中“}”在正则表达式中属于特殊字符,正则表达式会做一些判断,但是经过quote处理之后,就做为字面常量来处理,而不是做特殊字符处理了,最终模式串aa{path}cc}会被转换为下面这个正则表达式
\Qaa\E(.*)\Qcc}\E 被\Q \E包裹的就做为字面常量处理,增加效率
通过上面的一系列操作,就可以将传入的pattern模式串转换为正则表达式,放入this.pattern中,
this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) :Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));//最终生成一个正则表达式,caseSensitive用来区分大小写的
在回来看matchStrings方法,getStringMatcher方法的到一个内部类AntPathStringMatcher对象,然后调用它的方法matchStrings
private boolean matchStrings(String pattern, String str,
@Nullable Map<String, String> uriTemplateVariables) {
return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);//getStringMatcher其内部实现就是将Ant风格的模式串转为正则表达式然后去和参数str做匹配
}
matchStrings方法如下:
public boolean matchStrings(String str, @Nullable Map<String, String> uriTemplateVariables) { //变量解析
Matcher matcher = this.pattern.matcher(str);
if (matcher.matches()) { //如果不匹配直接返回false,如果不返回就
if (uriTemplateVariables != null) {
// SPR-8455
if (this.variableNames.size() != matcher.groupCount()) {
throw new IllegalArgumentException("The number of capturing groups in the pattern segment " +
this.pattern + " does not match the number of URI template variables it defines, " +
"which can occur if capturing groups are used in a URI template regex. " +
"Use non-capturing groups instead.");
}
for (int i = 1; i <= matcher.groupCount(); i++) {
String name = this.variableNames.get(i - 1);
String value = matcher.group(i);
uriTemplateVariables.put(name, value);
}
}
return true;
}
else {
return false;
}
}
可以看到,如果不匹配直接返回false,如果匹配返回true,如果uriTemplateVariables != null,就去提取模版变量variableNames。
至此就是matchStrings的实现,最终就是通过AntPathStringMatcher将传入的pattern模式串(这个模式串是Ant风格的,也可能含有正则表达式,比如上文的{path:\w+}中的\w+)中对应的通配符转换成正则表达式,然后与传参路径字符串进行匹配。注:大家最好跟着源码一起看本篇文章,或者自己写例子调试,一步一步,就可以分析清楚了。