Spring中的AntPathMatcher详解

Lou.Chen2022年12月14日
大约 11 分钟

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,可能是空串)
  • Map<String, String> extractUriTemplateVariables(String pattern, String path)

    • 提取path中模板变量。举例:/api/yourbatman/{age}为pattern,/api/yourbatman/18为path,那么该方法返回结果为Map值为{"age" : 18}
  • 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风格的匹配器的拼接原则如下:

image-20221214163030981

记得@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}")
  • ...