Spring源码之AntPathMatcher(一):doMatch算法

AntPathMatcher概述

首先,AntPathMatcher是什么?有什么能力?
AntPathMather是Spring提供的用于对资源路径进行解析或者对url的字符串做匹配用的。无论是资源路径还是url字符串都是采用的是Ant格式。
不懂?看下面:
相信大家都搭建过SSM的练手项目,然后在applicationContext.xml配置过Spring整合Mybatis吧,那下面的xml你肯定不陌生

    <!-- mybatis和spring完美整合,不需要mybatis的配置映射文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 扫描model包 -->
        <property name="typeAliasesPackage" value="com.cz.entity"/>
        <!-- 扫描sql配置文件:mapper需要的xml文件-->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

请注意配置扫描mapper需要的xml文件时用到了classpath:mapper/*.xml,其中的*就是Ant风格的模式串,模式串就相当于模版,符合这个模式、模版的都会被匹配到。Spring在解析xml时,就是通过AntPathMather的处理,扫描到所有mapper下面的所有xml文件的。同时,我们配置接口路径时一定用过@RequestMapping吧,如下

@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private IUserService userService;

    @RequestMapping("/select")
    public ModelAndView selectUser() throws Exception {
        ModelAndView mv = new ModelAndView();
        User user = userService.selectUser(1);
        mv.addObject("user", user);
        mv.setViewName("user");
        return mv;
    }
}

像这种url:/user/select可以通过AntPathMatcher精准匹配到的。现在知道了AntPathMatcher的作用了吧。

废话不多说,开始看源码。AntPathMatcher的源码重要的分为以下两点:

  1. AntPathMatcher的doMatch方法中的算法,将Ant模式的通配符和字符串进行分隔成数组,然后进行对应的匹配算法
  2. AntPathMatcher的内部类AntPathStringMatcher将Ant模式的通配符转换成java正则表达式,然后与路径字符串进行匹配

这篇文章要解析的是doMatch方法中的算法(基于spring-framework-5.2.8版本)

doMatch方法

概述:

下面这个是复杂的路径匹配:

boolean result = antPathMatcher.match("/aa/**/**/ff/ee/**/jj/**/bb/cc", "/aa/ee/ff/ee/jj/bb/cc");

match方法调用的是doMatch方法,doMatch方法中实现算法:

@Override
public boolean match(String pattern, String path) {  //match方法,匹配模式pattern和字符串path
	return doMatch(pattern, path, true, null);
}

在我们例子中,模式串和匹配的路径字符串分别是

模式串  pattern:"/aa/**/**/ff/ee/**/jj/**/bb/cc"
路径字符串  path:"/aa/ee/ff/ee/jj/bb/cc"

那么是如何进行匹配,去判断这个字符串符合这个模式串的规定的格式呢?doMatch的做法是将模式串和路径字符串都根据“/”分隔符分隔成一个数组,然后对两个数组,先从前朝后,一个一个对比,当遇到“**”时,退出循环,然后再从后往前一个一个对比,当再次遇到“**”时,就从之前从前朝后遇到“**”的地方开始往后找到第一个不是“**”的pattern,然后与path对应位置进行匹配,并且循环这个过程,直到pattern数组耗尽或者path数组耗尽。在从前朝后,从后超前的过程中,有些特殊情况可以直接判断出一定能匹配或者一定不能匹配,这些情况提前结束算法,提高算法效率。好了,大致过程就是如此,下面我们一步一步分析下源码。

第一步:将模式串和字符串都分隔成数组:
if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {  //判断此时path和pattern是否都是以分隔符"/"开头,或者path为null,就直接返回false
	return false;
}
String[] pattDirs = tokenizePattern(pattern);  //将pattDirs按照分隔符"/"进行分隔成数组,比如"/aa/**/bb/**/cc"分隔成{"aa","**","bb","**","cc"}
if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) {//isPotentialMatch方法粗略的判断path和pattDirs是否匹配,不匹配就返回false
	return false;
}
//得到两个数组,pattDirs和pathDirs,都是根据分隔符"/"分隔而成的数组,
String[] pathDirs = tokenizePath(path);

可以看到,分隔成的数组分别放在pattDirs和pathDirs,当然可以提前确定一定不匹配的提前返回false提高算法的效率。比如第一个if判断是否都是以“/”开头,不是的话肯定不匹配,直接返回false。在我们的例子中分隔成的数组分别为:

pattDirs:【“aa”,**,**,“ff”,“ee”,**,“jj”,**,“bb”,“cc”】
pathDirs:【“aa”,“ee”,“ff”,“ee”,“jj”,“bb”,“cc”】
第二步:数组从前往后循环进行匹配,遇到“**”停止

先定义数组对应的开始位置变量和末尾位置变量:

int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;

从前往后循环进行匹配,遇到“**”停止,匹配不上就结束算法返回false,其中matchStrings就是判断pattern模式和对应的字符串是否匹配的,下文中会讲,就先不关注其细节

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++;
}

按照我们前面讲的算法概述这个时候应该再从后往前进行循环遍历对比了,但是,在从前往后遍历遇到“**”停止后,有些情况是能判断一定匹配或者一定不匹配的,下面的判断就是

第三步:从前往后的遍历遇到“**”停止后,有些情况是能判断一定匹配或者一定不匹配,就可以提前结束算法

这种情况又分为三种情况:
1.path数组元素耗尽
2.pattern数组元素耗尽
3.!fullMatch情况(不赘述)
如下:

if (pathIdxStart > pathIdxEnd) { //path耗尽的情况
	// Path is exhausted, only match if rest of pattern is * or **'s
	if (pattIdxStart > pattIdxEnd) {  //如果此时正好patt也耗尽,就比较结尾字符是否相同,相同就返回true,说明匹配上了
		return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
	}
	if (!fullMatch) {
		return true;
	}

	//path耗尽,但是patt没耗尽,正好pattIdxStart == pattIdxEnd,也就是还剩一个字符串,如果这个字符串equals("*"),就是*的话,说明也比配
	if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {  //pattern:"/aa/bb/*"和path:"/aa/bb/"的情况
		return true;
	}

	//path耗尽,但是patt没耗尽,不止一个,判断如果都是**,也是匹配的 //pattern:"/aa/bb/**/**"和path:"/aa/bb/"的情况
	for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
		if (!pattDirs[i].equals("**")) {
			return false;
		}
	}
	return true;
}
path数组元素耗尽

可以看到,当pathIdxStart > pathIdxEnd也就是path数组元素耗尽的时候,进行下面的判断:
1.当path耗尽时,pattern也正好耗尽,如果path字符串和pattern模式串都是以“/”结束,说明其一定匹配。就比如下面这种情况

boolean result = antPathMatcher.match("/aa/bb/**", "/aa/bb");

2.!fullMatch情况(这里不赘述)
3.当path耗尽时,pattern也正好只剩下一个字符也就是pattIdxStart == pattIdxEnd,如果这个字符正好是*,此时也能匹配,就比如下面这种情况

boolean result = antPathMatcher.match("/aa/bb/**/*", "/aa/bb");

4.当path耗尽时,pattern没有耗尽,不管pattern剩下几个,判断如果剩下的都是“**”,则可以匹配,返回true,而哪怕又一个不是“**”,说明一定不能匹配,返回false。就就比如下面这种情况一定能匹配

boolean result = antPathMatcher.match("/aa/bb/**/**/**", "/aa/bb");
pattern数组元素耗尽

简单,path还没耗尽,而pattern模式串已经耗尽了,一定匹配不上了

else if (pattIdxStart > pattIdxEnd) {//pattern耗尽,但path没有耗尽,肯定不能相匹配了
	// String not exhausted, but pattern is. Failure.
	return false;
}

上面就是能直接判断一定不匹配或者一定匹配的。判断了这些能提前结束算法的情况后,我们再从后往前遍历,如下,

while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { //pattern:"/aa/**/bb/cc"和path:"/aa/bb/cc"的情况,此时从后往前遍历对比,遇到"**"退出
	String pattDir = pattDirs[pattIdxEnd];
	if (pattDir.equals("**")) {
		break;
	}
	if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
		return false;
	}
	pattIdxEnd--;
	pathIdxEnd--;
}

遇到“**”就结束循环,此时通过pattIdxStart和pattIdxEnd锁定了前后分别第一次“**”出现的位置,在我们的例子中也就是如下

pattDirs:【“aa”,**,**,“ff”,“ee”,**,“jj”,**,“bb”,“cc”】
             0    1    2    3    4    5    6    7    8    9
            
pathDirs:【“aa”,“ee”,“ff”,“ee”,“jj”,“bb”,“cc”】
            0     1    2    3    4    5    6
            
pattIdxStart=1
pattIdxEnd=7

pathIdxStart=1
pathIdxEnd=4

当然在结束循环后如果正好path耗尽就能判断一定匹配或一定不匹配:

if (pathIdxStart > pathIdxEnd) { //path耗尽,这时patt没有耗尽,就遍历中间的是不是只剩下"**",如果有不为"**"的返回false说明不匹配
	// String is exhausted
	for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
		if (!pattDirs[i].equals("**")) {//如果有不为"**"的返回false说明不匹配
			return false;
		}
	}
	return true;
}

那么我们知道,前面已经锁定了前后分别第一次“**”出现的位置,那么中间的我们怎么去匹配判断呢,如下

while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { //pattern:"/aa/**/**/ff/**/jj/**/bb/cc"和path:"/aa/ee/ff/bb/cc"的情况,终止条件为patt被耗尽或者path被耗尽.这是最复杂的一种算法情况
	int patIdxTmp = -1;
	for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {//从第一个**的位置+1开始遍历,遇到"**"停止,但是将**的位置索引记录在patIdxTmp中
		if (pattDirs[i].equals("**")) {
			patIdxTmp = i;
			break;
		}
	}
	if (patIdxTmp == pattIdxStart + 1) {//将记录的patIdxTmp(第一个**的位置+1) 与当前patt位置进行比较,相等的话就说明遇到了**/**的情况,然后continue跳出此次循环再循环一次,用于一直排除连续**的情况
		// '**/**' situation, so skip one
		pattIdxStart++;
		continue;
	}
	// Find the pattern between padIdxStart & padIdxTmp in str between
	// strIdxStart & strIdxEnd
	int patLength = (patIdxTmp - (pattIdxStart + 1));//计算剩下的patt数量
	int strLength = (pathIdxEnd - pathIdxStart + 1);//计算剩下的path数量
	int foundIdx = -1;

	strLoop:
	for (int i = 0; i <= strLength - patLength; i++) {//循环剩下的patt(其实patLength始终为1),和path,直到找到path中能和patt中ff相匹配的,此时返回path中ff的位置
		for (int j = 0; j < patLength; j++) {
			String subPat = pattDirs[(pattIdxStart  + 1) + j];
			String subStr = pathDirs[pathIdxStart + i + j];//这里的j只会为0
			if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
				continue strLoop;
			}
		}
		foundIdx = pathIdxStart + i;
		break;
	}

	if (foundIdx == -1) {
		return false;
	}

	pattIdxStart = patIdxTmp;//再从patIdxTmp位置,其实也就是ff后面的**的位置,从这个位置开始继续循环这个while,直到patt耗尽或者path耗尽
	pathIdxStart = foundIdx + patLength;//同理path循环也是从发现与ff匹配的索引的下一位开始。直到
}

这里的最为复杂,我们慢慢看。
下面这段代码,从第一次出现“**”的位置的下一个位置(pattIdxStart + 1)开始,循环遍历,找到下一次“**”出现的位置在我们的例子中就是下面这样,找到索引为5,这里又一点要说明,如果找到连续的“**”那就跳出此次循环,从下一个位置循环遍历。

for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {//从第一个**的位置+1开始遍历,遇到"**"停止,但是将**的位置索引记录在patIdxTmp中
	if (pattDirs[i].equals("**")) {
		patIdxTmp = i;
		break;
	}
}
if (patIdxTmp == pattIdxStart + 1) {//将记录的patIdxTmp(第一次**的位置) 与当前pattIdxStart + 1位置进行比较,相等的话就说明遇到了**/**的情况,然后continue跳出此次循环再循环一次,用于排除连续**的情况
	// '**/**' situation, so skip one
	pattIdxStart++;
	continue;
}

而后面要做的就是计算上一步中两个“**”中间的长度patLength和剩下path的长度strLength,然后计算之间的差值,我把这个差值叫做平移量i,而下面代码做的操作我把它叫做平移比较

strLoop:
for (int i = 0; i <= strLength - patLength; i++) { //循环次数由剩下的path长度和剩下的pattern长度决定
	for (int j = 0; j < patLength; j++) {
		String subPat = pattDirs[(pattIdxStart  + 1) + j];
		String subStr = pathDirs[pathIdxStart + i + j];
		if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
			continue strLoop;
		}
	}
	foundIdx = pathIdxStart + i;
	break;
}

if (foundIdx == -1) {
	return false;
}

在我们举的例子中就是下面两次比较

第一次:   ff    ee
         ee    ff    ee   jj
         
不匹配,i平移一位

第二次        ff    ee
       ee    ff    ee    jj
匹配成功 break

所以循环的数量为剩下path的数量减去剩下pattern的数量,这样也就知道平移几次就对比完了。
在代码中subPat和subStr的索引都+j也就保证了是对应位置比较的,第一位和第一位比较,第二位和第二位比较,

String subPat = pattDirs[(pattIdxStart  + 1) + j];
String subStr = pathDirs[pathIdxStart + i + j];

而如果第一位都不匹配,说明这一次平移比较是不匹配的,直接跳出此次平移比较,进入下一次

if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
	continue strLoop;
}

而下面代码中的+i保证了平移的进行

String subStr = pathDirs[pathIdxStart + i + j];

最后如果匹配成功,将匹配成功的位置记录下来foundIdx = pathIdxStart + i,其实就是pathIdxStart平移到的位置,当然,如果平移比较没有匹配成功,说明匹配不上,返回false

if (foundIdx == -1) {
	return false;
}

如果匹配成功了,就从后面那个“**”的位置起再循环一次这个操作,此时path开始循环的位置也改为了匹配上的字符串的后面那一位,也就是foundIdx + patLength,

pattIdxStart = patIdxTmp; //pattern循环从后面那个“**”的位置开始
pathIdxStart = foundIdx + patLength;//path循环也是从发现与ff/ee匹配的索引的下一位开始

上面平移比较都完成后,如果path耗尽了,pattern还有剩余,就判断是否都为**,如果不是说明不匹配。

//上面比较完了之后,path耗尽了,patt还有剩余,就判断是否都为**,如果不是说明不匹配
//就比如这种boolean result = antPathMatcher.match("/aa/**/**/ff/jj/**/ww/**/bb/cc", "/aa/ee/ff/jj/bb/cc");在ff/jj上面就已经比较完了,但此时模式串还剩ww没有匹配
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
	if (!pattDirs[i].equals("**")) {
		return false;
	}
}

最后如果不匹配的情况都返回false了,都排除了,就说明是匹配的,返回true

return true;

可以看出,源码中的逻辑严谨,全覆盖了所有匹配场景。并且可以做出判断的就提前结束算法,提高效率。
可以看到算法中经常遇到matchStrings,这个就是用到的AntPathMatcher的内部类AntPathStringMatcher,将Ant模式的通配符转换成java正则表达式,然后与路径字符串进行匹配,下一篇将介绍这个AntPathStringMatcher:Spring源码之AntPathMatcher:内部类AntPathStringMatcher

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值