Spring5新宠:PathPattern,AntPathMatcher:那我走?(下)

简介: Spring5新宠:PathPattern,AntPathMatcher:那我走?(下)

快速创建缺省的实例


上面了解到,PathPattern的构造器不是public的,所以有且仅能通过PathPatternParser创建其实例。然而,为快速满足绝大多数场景,Spring还提供了一种快速创建缺省的PathPattern实例的方式:


image.png


PathPatternParser提供一个全局共享的、只读的实例用于快速创建缺省的PathPattern实例,类似于实例工厂的作用。毕竟绝大部分场景下用PathPattern的缺省属性即可,因此有了它着实方便不少。


注意:虽然该PathPatternParser实例是全局共享只有1个,但是,创建出来的PathPattern可是不同实例哦(基本属性都一样而已)


代码示例


PathPattern的匹配方式和AntPathMatcher基本保持一致:使用的基于Ant风格模式匹配。


但是发现没,这里不再强调Ant字样,也许Spring觉得Ant的概念确实已廉波老矣?不符合它紧跟潮流的身份?


相比于AntPathMatcher,PathPattern主要有两处地方不一样:


说明:PathPattern只支持两种分隔符(/和.),而AntPathMatcher可以随意指定。虽然这也是不同点,但这一般无伤大雅所以就不单独列出了


1. 新增{*pathVariable}语法支持


这是PathPattern新增的“语法”,表示匹配余下的path路径部分并将其赋值给pathVariable变量。


@Test
public void test1() {
    System.out.println("======={*pathVariable}语法======");
    PathPattern pattern = PathPatternParser.defaultInstance.parse("/api/yourbatman/{*pathVariable}");
    // 提取匹配到的的变量值
    System.out.println("是否匹配:" + pattern.matches(PathContainer.parsePath("/api/yourbatman/a/b/c")));
    PathPattern.PathMatchInfo pathMatchInfo = pattern.matchAndExtract(PathContainer.parsePath("/api/yourbatman/a/b/c"));
    System.out.println("匹配到的值情况:" + pathMatchInfo.getUriVariables());
}
======={*pathVariable}语法======
是否匹配:true
匹配到的值情况:{pathVariable=/a/b/c}


在没有PathPattern之前,虽然也可以通过/**来匹配成功,但却无法得到匹配到的值,现在可以了!


和**的区别


我们知道/**和/{*pathVariable}都有匹配剩余所有path的“能力”,那它俩到底有什么区别呢?


  1. /**能匹配成功,但无法获取到动态成功匹配元素的值
  2. /{*pathVariable}可认为是/**的加强版:可以获取到这部分动态匹配成功的值


正所谓一代更比一代强嘛,如是而已。


和**的优先级关系


既然/**和/{*pathVariable}都有匹配剩余path的能力,那么它俩若放在一起,优先级关系是怎样的呢?


妄自猜测没有意义,跑个案例一看便知:由于PathPattern实现了比较器接口,因此本例利用SortedSet自动排序即可,排第一的证明优先级越高


@Test
public void test2() {
    System.out.println("======={*pathVariable}和/**优先级======");
    PathPattern pattern1 = PathPatternParser.defaultInstance.parse("/api/yourbatman/{*pathVariable}");
    PathPattern pattern2 = PathPatternParser.defaultInstance.parse("/api/yourbatman/**");
    SortedSet<PathPattern> sortedSet = new TreeSet<>();
    sortedSet.add(pattern1);
    sortedSet.add(pattern2);
    System.out.println(sortedSet);
}
======={*pathVariable}和/**优先级======
[/api/yourbatman/**, /api/yourbatman/{*pathVariable}]

测试代码的细节:故意将/{*pathVariable}先放进set里面而后放/**,但最后还是/**在前。


结论:当二者同时出现(出现冲突)时,/**优先匹配。


2. 禁用中间**语法支持


在上篇文章对AntPathMatcher的详细分析文章中,我们知道是可以把/**放在整个URL中间用来匹配的,如:


@Test
public void test4() {
    System.out.println("=======**:匹配任意层级的路径/目录=======");
    String pattern = "/api/**/yourbatman";
    match(1, MATCHER, pattern, "/api/yourbatman");
    match(2, MATCHER, pattern, "/api//yourbatman");
    match(3, MATCHER, pattern, "/api/a/b/c/yourbatman");
}
=======**:匹配任意层级的路径/目录=======
1 match结果:/api/**/yourbatman  【成功】  /api/yourbatman
2 match结果:/api/**/yourbatman  【成功】  /api//yourbatman
3 match结果:/api/**/yourbatman  【成功】  /api/a/b/c/yourbatman


AntPathMatcher不同,**仅在模式末尾受支持。中间不被允许了,否则实例创建阶段就会报错:


@Test
public void test3() {
    System.out.println("=======/**放在中间语法======");
    PathPattern pattern = PathPatternParser.defaultInstance.parse("/api/**/yourbatman");
    pattern.matches(PathContainer.parsePath("/api/a/b/c/yourbatman"));
}
=======/**放在中间语法======
org.springframework.web.util.pattern.PatternParseException: No more pattern data allowed after {*...} or ** pattern element
  at org.springframework.web.util.pattern.InternalPathPatternParser.peekDoubleWildcard(InternalPathPatternParser.java:250)
  ...



从报错中还能看出端倪:不仅**,{*xxx}也是不能放在中间而只能是末尾的


PathPattern这么做的目的是:消除歧义。


那么问题来了,如果就是想匹配中间的任意层级路径怎么做呢?

答:首先这在web环境里有这样需求的概率极小(PathPattern只适用于web环境),若这依旧是刚需,那就只能蜕化到借助AntPathMatcher来完成喽。


PathPattern对比AntPathMatcher


二者目前都存在于Spring技术栈内,做着“相同”的事。虽说现在还鲜有同学了解到PathPattern,我认为淘汰掉AntPathMatcher只是时间问题(特指web环境哈),毕竟后浪总归有上岸的一天。


但不可否认,二者将在较长时间内共处,那么它俩到底有何区别呢?了解一下


出现时间


AntPathMatcher是一个早在2003年(Spring的第一个版本)就已存在的路径匹配器,而PathPattern是Spring 5新增的,旨在用于替换掉较为“古老”的AntPathMatcher。


功能差异


PathPattern去掉了Ant字样,但保持了很好的向下兼容性:除了不支持将**写在path中间之外,其它的匹配规则从行为上均保持和AntPathMatcher一致,并且还新增了强大的{*pathVariable}的支持。


因此在功能上姑且可认为二者是一致的,极特殊情况下的不兼容除外。


性能差异


Spring官方说PathPattern的性能优于AntPathMatcher,我抱着怀疑的态度做了测试,示例代码和结果如下:

// 匹配的模板:使用一个稍微有点复杂的模板进行测试
private static final String pattern = "/api/your?atman/{age}/**";


// AntPathMatcher匹配代码:使用单例的PathMatcher,符合实际使用情况
private static final PathMatcher MATCHER = new AntPathMatcher();
public static void antPathMatcher(String reqPath) {
    MATCHER.match(reqPath);
}


// PathPattern代码示例:这里的pattern由下面来定义
private static final PathPattern PATTERN = PathPatternParser.defaultInstance.parse(pattern);
public static void pathPattern(String reqPath) {
    PATTERN.matches(PathContainer.parsePath(reqPath));
}

匹配的测试代码:


@Test
public void test1() {
    Instant start = Instant.now();
    for (int i = 0; i < 100000; i++) {
        String reqPath = "/api/yourBatman/" + i + "/" + i;
        antPathMatcher(reqPath);
        // pathPattern(reqPath);
    }
    System.out.println("耗时(ms):" + Duration.between(start, Instant.now()).toMillis());
}


不断调整循环次数,且各执行三次,将结果绘制成如下表格:


image.png


循环100000次:


image.png


循环1000000次:


image.png


循环10000000次:


image.png


结论:PathPattern性能比AntPathMatcher优秀。理论上pattern越复杂,PathPattern的优势越明显。


最佳实践


既然路径匹配器有两种方案,那必然有最佳实践。Spring官方对此也是持有态度的:


Web环境


如果是Servlet应用(webmvc),官方推荐PathPattern(只是推荐,但默认的依旧是AntPathMatcher哈),相关代码体现在PathPattern里:

// Since: 07.04.2003
public abstract class AbstractHandlerMapping ... {
  private UrlPathHelper urlPathHelper = new UrlPathHelper();
  private PathMatcher pathMatcher = new AntPathMatcher();
  ...
  @Nullable
  private PathPatternParser patternParser;
  // Since: 5.3
  public void setPatternParser(PathPatternParser patternParser) {
    this.patternParser = patternParser;
  }
}


注意:setPatternParser()从5.3版本开始才被加入,也就说虽然PathPattern从Spring 5就有了,但直到5.3版本才被加入到webmvc里,且作为可选(默认依旧是AntPathMatcher)。换句话讲:在Spring 5.3版本之前,仍旧只能用AntPathMatcher。


在WebMvc里启用PathPattern


默认情况下,Spring MVC依旧是使用的AntPathMatcher进行路径匹配的,那如何启用效率更高的PathPattern呢?


通过上面源码知道,就是要调用AbstractHandlerMapping的setPatternParser方法嘛,其实Spring为此是预留了扩展点的,只需这么做即可:


/**
 * 在此处添加备注信息
 *
 * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
 * @site https://yourbatman.cn
 * @date 2021/6/20 18:33
 * @since 0.0.1
 */
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setPatternParser(PathPatternParser.defaultInstance);
    }
}


如果是Reactor应用(webflux),那PathPattern就是唯一解决方案。这体现在org.springframework.web.reactive.handler.AbstractHandlerMapping

// Since: 5.0
public abstract class AbstractHandlerMapping... {
  private final PathPatternParser patternParser;
  ...
  public AbstractHandlerMapping() {
    this.patternParser = new PathPatternParser();
  }
}


webflux里早已不见AntPathMatcher的踪影,因为webflux是从Spring 5.0开始的,因此没有向下兼容的负担,直接全面拥抱PathPattern了。


结论:PathPattern语法更适合于web应用程序,其使用更方便且执行更高效。

非Web环境


嗯,如果认真“听课”了的同学就知道:非Web环境依旧有且仅有一种选择,那便是AntPathMatcher,因为PathPattern是专为Web环境设计,不能用于非Web环境。所以像上面资源加载、包名扫描之类的,底层依旧是交给AntPathMatcher去完成。


说明:由于这类URL的解析绝大多数情况下匹配一次(执行一次)就行,所以微小的性能差异是无所谓的(对API来讲收益较大)


可能有小伙伴会说:在Service层,甚至Dao层我也可以正常使用PathPattern对象呀,何解?

这个问题就相当于:HttpServletRequest属于web层专用组件,但你依旧可以将其传到Service层,甚至Dao层供以使用,在编译、运行时不会报错。但你可深入思考下,这么做合适吗?


举个生活上的例子:马桶可以装在卫生间,也可以安装在卧室的床旁边,都能完成大小便功能,但你觉得这么做合适吗?


Java这门语言对访问权限的控制设计得还是很优秀的,很多隔离性的问题在编译器就能搞定。但有很多规范性做法是无法做到强约束的,只能依靠工程师自身水平。这就是经验,也是区别初级工程师和高级工程师的重要因素。


总结


技术的日新月异,体现在一个个像PathPattern这个更好的API上。


Spring 5早在2017-09就已发布,可能是由于它“设计得过于优秀”,即使大版本的发布也几乎保持100%向下兼容,使得一般开发者感受不到它的升级。但是,这对框架二次开发者并不可能完全透明,因为二次开发经常会用到其Low-Level的API,比如今天的主角PathPattern就算其中之一,所以说我们要与时俱进呀o(╥﹏╥)o!


Spring 5虽然新增了(更好的)PathPattern,但它不能完全替代掉AntPathMatcher,因为前者专为web设计,所以在web领域是可完全替代掉AntPathMatcher的。但在非web领域内,AntPathMatcher依旧不可替代。

相关文章
|
Java API 网络架构
Spring5新宠:PathPattern,AntPathMatcher:那我走?(上)
Spring5新宠:PathPattern,AntPathMatcher:那我走?(上)
Spring5新宠:PathPattern,AntPathMatcher:那我走?(上)
|
6月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
1110 0
|
7月前
|
人工智能 Java 测试技术
Spring Boot 集成 JUnit 单元测试
本文介绍了在Spring Boot中使用JUnit 5进行单元测试的常用方法与技巧,包括添加依赖、编写测试类、使用@SpringBootTest参数、自动装配测试模块(如JSON、MVC、WebFlux、JDBC等),以及@MockBean和@SpyBean的应用。内容实用,适合Java开发者参考学习。
868 0
|
3月前
|
JavaScript Java Maven
【SpringBoot(二)】带你认识Yaml配置文件类型、SpringMVC的资源访问路径 和 静态资源配置的原理!
SpringBoot专栏第二章,从本章开始正式进入SpringBoot的WEB阶段开发,本章先带你认识yaml配置文件和资源的路径配置原理,以方便在后面的文章中打下基础
422 3
|
3月前
|
Java 测试技术 数据库连接
【SpringBoot(四)】还不懂文件上传?JUnit使用?本文带你了解SpringBoot的文件上传、异常处理、组件注入等知识!并且带你领悟JUnit单元测试的使用!
Spring专栏第四章,本文带你上手 SpringBoot 的文件上传、异常处理、组件注入等功能 并且为你演示Junit5的基础上手体验
941 2
|
10月前
|
前端开发 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
464 0
|
10月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
565 0
|
10月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
188 0
|
10月前
|
SQL Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— application.yml 中对日志的配置
在 Spring Boot 项目中,`application.yml` 文件用于配置日志。通过 `logging.config` 指定日志配置文件(如 `logback.xml`),实现日志详细设置。`logging.level` 可定义包的日志输出级别,例如将 `com.itcodai.course03.dao` 包设为 `trace` 级别,便于开发时查看 SQL 操作。日志级别从高到低为 ERROR、WARN、INFO、DEBUG,生产环境建议调整为较高级别以减少日志量。本课程采用 yml 格式,因其层次清晰,但需注意格式要求。
1025 0
|
6月前
|
缓存 JSON 前端开发
第07课:Spring Boot集成Thymeleaf模板引擎
第07课:Spring Boot集成Thymeleaf模板引擎
635 0
第07课:Spring Boot集成Thymeleaf模板引擎