Spring中的AntPathMatcher详解
PathMatcher路径匹配器
PathMatcher是抽象接口,该接口抽象出了路径匹配器的概念,用于对path路径进行匹配。它提供如下方法,在Spring中,只有AntPathMatcher
类有对其实现
PathMatcher所在的包为
org.springframework.util.PathMatcher
,属于spring-core核心模块,表示它可运用在任意模块,not only for web。
// Since: 1.2
public interface PathMatcher {
boolean isPattern(String path);
boolean match(String pattern, String path);
boolean matchStart(String pattern, String path);
String extractPathWithinPattern(String pattern, String path);
Map<String, String> extractUriTemplateVariables(String pattern, String path);
Comparator<String> getPatternComparator(String path);
String combine(String pattern1, String pattern2);
}
boolean isPattern(String path)
- 判断path是否是一个模式字符串(一般含有指定风格的特殊通配符就算是模式了)
boolean match(String pattern, String path)
- 最重要的方法。判断path和模式pattern是否匹配(注意:二者都是字符串,传值不要传反了哈)
boolean matchStart(String pattern, String path)
- 判断path是否和模式pattern前缀匹配(前缀匹配:path的前缀匹配上patter了即可,当然全部匹配也是可以的)
String extractPathWithinPattern(String pattern, String path)
- 返回和pattern模式真正匹配上的那部分字符串。举例:
/api/yourbatman/*.html
为pattern,/api/yourbatman/form.html
为path,那么该方法返回结果为form.html
(注意:返回结果永远不为null,可能是空串)
- 返回和pattern模式真正匹配上的那部分字符串。举例:
Map<String, String> extractUriTemplateVariables(String pattern, String path)
- 提取path中模板变量。举例:
/api/yourbatman/{age}
为pattern,/api/yourbatman/18
为path,那么该方法返回结果为Map值为{"age" : 18}
- 提取path中模板变量。举例:
Comparator<String> ComparatorgetPatternComparator(String path)
- 路径比较器,用于排序确定优先级高低
String combine(String pattern1, String pattern2)
- 合并两个pattern模式,组合算法由具体实现自由决定
该接口规定了作为路径匹配器一些必要的方法,同时也开放了一些行为策略如getPatternComparator、combine
等由实现类自行决定。
Ant风格
Ant风格(Ant Style):该风格源自Apache的Ant项目
Ant风格简单的讲,它是一种精简的匹配模式,仅用于匹配路径or目录。
通配符 | 说明 |
---|---|
? | 匹配任意单字符 |
* | 匹配任意数量的字符 |
** | 匹配任意层级的路径/目录 |
实践场景举例
在自定义的登录过滤器中,经常会放行一些API接口让免登录即可访问,这是典型的URL白名单场景,这个时候就会涉及到URL的匹配方式问题,一般会有如下方案:
精确匹配:
url.equals("/api/v1/yourbatman/adress")
- 缺点:硬编码式一个个罗列,易造成错误且不好维护
前缀匹配:
url.startsWith("/api/v1/yourbatman")
- 这也算一种匹配模式,可以批量处理某一类URL。缺点是:匹配范围过大易造成误伤,或者范围过小无法形成有效匹配,总之就是欠缺灵活度
包含匹配:
url.contains("/yourbatman")
- 这个缺点比较明显:强依赖于URL的书写规范(如白名单的URL都必须包含指定子串),并且极易造成误伤
正则表达式匹配:
Pattern.compile("正则表达式").matcher(url).find()
- 它的最大优点是可以满足几乎任意的URL(包括精确、模式等),但最大的缺点是书写比较复杂,用时多少这和coder的水平强相关,另外这对后期维护也带来了一定挑战~
经常会听到这样一句话:“通过正则表达式或者Ant风格的路径表达式来做URL匹配”。正所谓“杀鸡何必用牛刀”,URL相较于普通的字符串具有很强的规律性:标准的分段式。
因此,使用轻量级Ant风格表达式作为URL的匹配模式更为合适:
轻量级执行效率高
通配符(模式)符合正常理解,使用门槛非常低
*
和**
对层级路径/目录的支持感觉就是为此而生的
对于复杂场景亦可包含正常表达式来达到通用性
AntPathMatcher
AntPathMatcher 基于Ant风格的路径匹配器
PathMatcher接口并未规定路径匹配的具体方式,在Spring的整个技术栈里(包括Spring Boot和Cloud)有且仅有一个实现类AntPathMatcher:基于Ant风格的路径匹配器。它运用在Spring
技术栈的方方面面,如:URL路径匹配、资源目录匹配等等。
前置代码准备工作
private static final AntPathMatcher ant=new AntPathMatcher();
/**
* 匹配方法
* @param index 索引,只是为了做区分
* @param ant AntPathMatcher匹配器
* @param pattern Ant路径规则
* @param reqPath 要匹配的路径
*/
static void match(int index, AntPathMatcher ant, String pattern, String reqPath) {
boolean match = ant.match(pattern, reqPath);
System.out.println(index + " 匹配结果:" + match + " 匹配路径:" + reqPath);
}
「 ? 」匹配任意单个字符
因为是匹配单字符,所以一般“夹杂”在某个path片段内容中间
String pattern="/api/he?lo";
match(1, ant, pattern, "/api/helllo");
match(2, ant, pattern, "/api/hello/123");
match(3, ant, pattern, "/api/hello");
match(4, ant, pattern, "/api/helloworld");
1 匹配结果:false 匹配路径:/api/helllo 2 匹配结果:false 匹配路径:/api/hello/123 3 匹配结果:true 匹配路径:/api/hello 4 匹配结果:false 匹配路径:/api/helloworld
可以看到,只有第3
个路径规则匹配成功。
?
表示匹配精确的1个字符- 即使
?
匹配成功,但“多余”部分和pattern并不匹配最终结果也会是false
「 * 」匹配任意数量的字符
因为是匹配任意数量的字符,所以一般使用*来代表URL的一个层级
String pattern="/api/*/hello";
match(1, ant, pattern, "/api/hello");
match(2, ant, pattern, "/api/ /hello");
match(3, ant, pattern, "/api//hello");
match(4, ant, pattern, "/api/123/hello");
match(5, ant, pattern, "/api/123/456/hello");
match(6, ant, pattern, "/api/123/hello/123");
1 匹配结果:false 匹配路径:/api/hello 2 匹配结果:true 匹配路径:/api/ /hello 3 匹配结果:false 匹配路径:/api//hello 4 匹配结果:true 匹配路径:/api/123/hello 5 匹配结果:false 匹配路径:/api/123/456/hello 6 匹配结果:false 匹配路径:/api/123/hello/123
可以看到,只有2,4
匹配成功
*
表示匹配到任意字符,并且必须至少有一个字符(包括空格)- 路径的//间必须有内容(即使是个空串)才能被
*
匹配到
- 路径的//间必须有内容(即使是个空串)才能被
*
只能匹配具体某一层的路径内容
「 ** 」匹配任意层级目录/路径
匹配任意层级的路径/目录,这对URL这种类型字符串及其友好。
放在中间:
String pattern="/api/**/hello";
match(1, ant, pattern, "/api/hello");
match(2, ant, pattern, "/api/123/456/hello");
match(3, ant, pattern, "/api/123/hello");
1 匹配结果:true 匹配路径:/api/hello 2 匹配结果:true 匹配路径:/api/123/456/hello 3 匹配结果:true 匹配路径:/api/123/hello
放在结尾:
String pattern="/api/hello/**";
match(1, ant, pattern, "/api/hello/123/456");
match(2, ant, pattern, "/api/hello");
match(3, ant, pattern, "/api/hello/");
1 匹配结果:true 匹配路径:/api/hello/123/456 2 匹配结果:true 匹配路径:/api/hello 3 匹配结果:true 匹配路径:/api/hello/
关注点:
**
的匹配“能力”非常的强,几乎可以匹配一切:任意层级、任意层级里的任意“东西”**
在AntPathMatcher里即可使用在路径中间,也可用在末尾**
位置的路径为空
也可以匹配
正则表达式匹配
前置代码
/**
* 路径解析方法
* @param index 索引,只是为了做区分
* @param ant AntPathMatcher匹配器
* @param pattern Ant路径规则
* @param reqPath 要匹配的路径
*/
static void extractUriTemplateVariables(int index, AntPathMatcher ant, String pattern, String reqPath) {
if (ant.match(pattern, reqPath)) {
Map<String, String> map = ant.extractUriTemplateVariables(pattern, reqPath);
System.out.println(index + " 匹配结果:" + map + " 匹配路径:" + reqPath);
}else{
System.out.println(index + " 匹配失败,无法解析"+ " 匹配路径:" + reqPath);
}
}
extractUriTemplateVariables方法
正则匹配示例:
String pattern="/api/hello/{age}";
extractUriTemplateVariables(1, ant, pattern, "/api/hello/20");
extractUriTemplateVariables(2, ant, pattern, "/api/hello/二十");
1 匹配结果:{age=20} 匹配路径:/api/hello/20 2 匹配结果:{age=二十} 匹配路径:/api/hello/二十
该语法的匹配规则为:将匹配到的path内容赋值给pathVariable。
但是一般需要规定age
的类型值,否则会匹配到二十
非数字类型的值。即通过正则
规定路径匹配的类型:
String pattern="/api/hello/{age:[0-9]*}";
extractUriTemplateVariables(1, ant, pattern, "/api/hello/20");
extractUriTemplateVariables(2, ant, pattern, "/api/hello/二十");
1 匹配结果:{age=20} 匹配路径:/api/hello/20 2 匹配失败,无法解析 匹配路径:/api/hello/二十
关注点:
AntPathMatcher
中的extractUriTemplateVariables
方法执行的前提需要先执行match
方法,即只有匹配
了才能进行路径解析,否则会抛出IllegalStateException
常用方法
isPattern
boolean isPattern(String path)
- 判断path是否是一个模式字符串(一般含有指定风格的特殊通配符就算是模式了)
一般包含? * ** {xxx}
这种特殊字符的字符串都属于模式
matchStart
boolean matchStart(String pattern, String path)
- 判断path是否和模式pattern前缀匹配(前缀匹配:path的前缀匹配上patter了即可,当然全部匹配也是可以的)
match和matchStart区别
- match:要求全路径完全匹配
- matchStart:模式部分匹配上,然后其它部分(若还有)是空路径即可
@Test
public void test8() {
System.out.println("=======matchStart方法=======");
String pattern = "/api/?";
System.out.println("match方法结果:" + MATCHER.match(pattern, "/api/y"));
System.out.println("match方法结果:" + MATCHER.match(pattern, "/api//"));
System.out.println("match方法结果:" + MATCHER.match(pattern, "/api////"));
System.out.println("matchStart方法结果:" + MATCHER.matchStart(pattern, "/api//"));
System.out.println("matchStart方法结果:" + MATCHER.matchStart(pattern, "/api////"));
System.out.println("matchStart方法结果:" + MATCHER.matchStart(pattern, "/api///a/"));
}
=======matchStart方法=======
match方法结果:true
match方法结果:false
match方法结果:false
matchStart方法结果:true
matchStart方法结果:true
matchStart方法结果:false
extractPathWithinPattern
String extractPathWithinPattern(String pattern, String path)
- 返回和pattern模式真正匹配上的那部分字符串。举例:
/api/yourbatman/*.html
为pattern,/api/yourbatman/form.html
为path,那么该方法返回结果为form.html
(注意:返回结果永远不为null,可能是空串)
@Test
public void test9() {
System.out.println("=======extractPathWithinPattern方法=======");
String pattern = "/api/*.html";
System.out.println("是否匹配成功:" + MATCHER.match(pattern, "/api/yourbatman/address")
+ ",提取结果:" + MATCHER.extractPathWithinPattern(pattern, "/api/yourbatman/address"));
System.out.println("是否匹配成功:" + MATCHER.match(pattern, "/api/index.html")
+ ",提取结果:" + MATCHER.extractPathWithinPattern(pattern, "/api/index.html"));
}
=======extractPathWithinPattern方法=======
是否匹配成功:false,提起结果:yourbatman/address
是否匹配成功:true,提起结果:index.html
关注点:
- 该方法和extractUriTemplateVariables()不一样,即使匹配不成功也能够返回参与匹配的那部分。
pattern里具有多个模式:
@Test
public void test10() {
System.out.println("=======extractPathWithinPattern方法=======");
String pattern = "/api/**/yourbatman/*.html/temp";
System.out.println("是否匹配成功:" + MATCHER.match(pattern, "/api/yourbatman/address")
+ ",提取结果:" + MATCHER.extractPathWithinPattern(pattern, "/api/yourbatman/address"));
System.out.println("是否匹配成功:" + MATCHER.match(pattern, "/api/yourbatman/index.html/temp")
+ ",提取结果:" + MATCHER.extractPathWithinPattern(pattern, "/api/yourbatman/index.html/temp"));
}
=======extractPathWithinPattern方法=======
是否匹配成功:false,提取结果:yourbatman/address
是否匹配成功:true,提取结果:yourbatman/index.html/temp
关注点:
该方法会返回所有参与匹配的片段,即使这匹配不成功
若有多个模式(如本例中的
**
和*
),返回的片段不会出现跳跃现象(只截掉前面的非pattern匹配部分,中间若出现非pattern匹配部分是不动的)
getPatternComparator
Comparator<String> ComparatorgetPatternComparator(String path)
- 路径比较器,用于排序确定优先级高低
此方法用于返回一个Comparator<String>
比较器,用于对多个path之间进行排序。目的:让更具体的 path出现在最前面,也就是所谓的精确匹配优先原则(也叫最长匹配规则(has more characters))。
@Test
public void test11() {
System.out.println("=======getPatternComparator方法=======");
List<String> patterns = Arrays.asList(
"/api/**/index.html",
"/api/yourbatman/*.html",
"/api/**/*.html",
"/api/yourbatman/index.html"
);
System.out.println("排序前:" + patterns);
Comparator<String> patternComparator = MATCHER.getPatternComparator("/api/yourbatman/index.html");
Collections.sort(patterns, patternComparator);
System.out.println("排序后:" + patterns);
}
=======getPatternComparator方法=======
排序前:[/api/**/index.html, /api/yourbatman/*.html, /api/**/*.html, /api/yourbatman/index.html]
排序后:[/api/yourbatman/index.html, /api/yourbatman/*.html, /api/**/index.html, /api/**/*.html]
关注点:
该方法拥有一个入参,作用为:用于判断是否是精确匹配,也就是用于确定精确值的界限的(根据此界限值进行排序)
越精确的匹配在越前面。其中路径的匹配原则是从左至右(也就是说左边越早出现精确匹配,分值越高)
combine
String combine(String pattern1, String pattern2)
- 合并两个pattern模式,组合算法由具体实现自由决定
将两个方法“绑定”在一起。PathMatcher接口并未规定绑定的“规则”,完全由底层实现自行决定。如基于Ant风格的匹配器的拼接原则如下:
记得@RequestMapping
这个注解吧,它既可以标注在类上,亦可标注在方法上。把Pattern1比作标注在类上的path(若木有标注值就是null嘛),把Pattern2比作标注在方法上的path,它俩的结果不就可以参考上图了麽。一不小心又加深了点对@RequestMapping
注解的了解有木有。
使用细节
/
默认分隔符为 public static final String DEFAULT_PATH_SEPARATOR = "/";
默认使用/
作为路径分隔符。若是请求路径(如http、RPC请求等)或者是Linux的目录名,一切相安无事。但若是Windows的目录地址呢?
说明:windows目录分隔符是
\
,如C:\ProgramData\Microsoft\Windows\Start Menu\Programs\7-Zip
可根据实际情况在构造时自行指定分隔符(如windows是\
,Lunux是/
,包名是.
)
在创建AntPathMatcher
对象时指定分隔符:
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
正则表达式判断
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?}");
当路径/目录里出现{xxx:正则表达式}
这种模式的字符串就被认定为是VARIABLE_PATTERN
总结
在应用层面,Ant风格的URL、目录地址、包名地址也是随处可见:
- 扫包:@ComponentScan(basePackages = "cn.yourbatman.**.controller")
- 加载资源:classpath: config/application-*.yaml
- URL映射:@RequestMapping("/api/v1/user/{id}")
- ...