阅读完需:约 9 分钟
Spring5 也已经出来好久了,里边有一些新玩法也需要我们去慢慢揭开面纱,最近在看 SpringMVC 源码的时候,就看到这样一段代码:
/**
* Initialize the path to use for request mapping.
* <p>When parsed patterns are {@link #usesPathPatterns() enabled} a parsed
* {@code RequestPath} is expected to have been
* {@link ServletRequestPathUtils#parseAndCache(HttpServletRequest) parsed}
* externally by the {@link org.springframework.web.servlet.DispatcherServlet}
* or {@link org.springframework.web.filter.ServletRequestPathFilter}.
* <p>Otherwise for String pattern matching via {@code PathMatcher} the
* path is {@link UrlPathHelper#resolveAndCacheLookupPath resolved} by this
* method.
* @since 5.3
*/
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
这段代码初始化用于请求映射的路径,通过PathMatcher进行的字符串模式匹配
这是在AbstractHandlerMapping抽象类中的方法,这个类在Spring WebFlux中起到了关键的作用,用于URL的匹配。当时就很好奇:这一直不都是AntPathMatcher的活?之前在学Security的时候也用到过。
上面的方法是 Spring5 里边出来的,以前是没有这个方法的。在旧的 SpringMVC 中,当我们需要获取当前请求地址的时候,直接通过如下方式获取:
String lookupPath = this.getUrlPathHelper().getLookupPathForRequest(request);
但是现在变了,现在获取当前请求 URL 地址时,方式如下:
String lookupPath = initLookupPath(request);
两种方式相比,主要是 initLookupPath 方法中多了 usesPathPatterns 选项,这是 Spring5 中的新玩意,如果你在项目中使用了 WebFlux,那么这个东西就显得尤为重要了!因为WebFlux只有PathPattern模式,没有AntPathMatcher。
这里测试的版本为
- SpringBoot: 2.4.2
- JDK: 8
- Spring Framework:5.3.19
AntPathMatcher
当我们使用 @RequestMapping 注解去标记请求接口的时候(或者使用它的类似方法如 @GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping),我们可以使用一些通配符去匹配 URL 地址,举个简单例子,假设我有下面五个接口:
@RestController
public class AntPathMatcherDemo {
@GetMapping("/hello/**/hello")
public String hello() {
return "/hello/**/hello";
}
@GetMapping("/h?llo")
public String hello2() {
return "/h?llo";
}
@GetMapping("/**/*.html")
public String hello3() {
return "/**/*.html";
}
@GetMapping("/hello/{p1}/{p2}")
public String hello4(@PathVariable String p1, @PathVariable String p2) {
System.out.println("p1 = " + p1);
System.out.println("p2 = " + p2);
return "/hello/{p1}/{p2}";
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
System.out.println("name = " + name);
System.out.println("version = " + version);
System.out.println("ext = " + ext);
}
}
说说这几个通配符的含义:
| 通配符 | 含义 |
|---|---|
** |
匹配0个或者多个目录 |
* |
匹配0个或者多个字符 |
? |
匹配任意单个字符 |
了解了通配符的含义,我们再来说说各个接口都能接收哪些请求:
- 第一个接口,可以接收诸如
/hello/123/123/hello、/hello/a/hello以及/hello/hello这样的请求,因为中间的**代表 0 个或者多个目录。 - 第二个接口,可以接收诸如
/hallo、/hello、/hMllo之类的请求,注意它不能接收/haallo或者/hllo,因为?表示一个字符。 - 第三个接口可以接收任意以
.html为后缀的请求,例如/aaa/bb/cc.html、/aa.html或者/aa/aa.html。 - 第四个接口估计大家都比较熟悉,在 RESTful 风格的接口设计中估计大家都用过,它接收的请求格式类似于
/hello/aa/bb,其中参数 p1 就对应 aa,参数 p2 对应 bb。 - 第五个接口则用到了正则,name、version 以及 ext 三个参数格式用正则表达出来,它可以接收诸如
/spring-web-3.0.5.jar格式的请求,最终的参数 name 就是spring-web,version 就是3.0.5,ext 则是.jar。
这是 SpringMVC 中之前就存在的功能,不管你用没用过,反正它一致存在。
那么是谁支撑了这个功能呢?那就是 AntPathMatcher。
AntPathMatcher 是一个实现了 Ant 风格的路径匹配器,Ant 风格的路径规则实际上就是我们前面给大家介绍的那三种路径匹配符,很 Easy。这种路径匹配规则源自 Apache Ant 项目(https://ant.apache.org),Apache Ant 我们现在其实已经很少会用到了,它的替代品就是大家所熟知的 Maven,如果你有幸维护一些 2010 年之前的老项目的话,有可能会接触到 Ant。
AntPathMatcher 实际上在 SpringMVC 中有非常广泛的应用,不仅仅是在 @RequestMapping 中定义接口用到,在其他一些涉及到地址匹配的地方也会用到,例如我们在 SpringMVC 的配置文件中配置静态资源过滤时,也是 Ant 风格路径匹配:
<mvc:resources mapping="/**" location="/"/>
另外像拦截器里的拦截路径注册、跨域处理时的路径匹配等等,都会用到 Ant 风格的路径匹配符。
整体上来说,AntPathMatcher 是 Spring 中一种比较原始的路径匹配解决方案,虽然比较简单,但是它的效率很低,并且在处理 URL 编码的时候也很不方便。
因此,才有了 Spring5 中的 PathPattern。
PathPattern
PathPattern 专为 Web 应用设计,它与之前的 AntPathMatcher 功能大部分比较类似,当然也有一些细微差异
如果是 Servlet 应用,目前官方推荐的 URL 匹配解决方案就是 PathPattern(当然你也可以选择较早的 AntPathMatcher),虽然官方推荐的是 PathPattern,但实际上默认使用的依然是 AntPathMatcher;如果你用的是 WebFlux,PathPattern 就是唯一解决方案了。
注意,PathPattern 是一个非常新鲜的玩艺,在 Spring5.3 之前,我们在 Servlet 应用中,也只能选择 AntPathMatcher,从 Spring5.3 之后,我们才可以使用 PathPattern 了。
PathPattern 会将 URL 规则预解析为 PathContainer,它对 URL 地址匹配的处理更加快速,PathPattern 与 AntPathMatcher 的差异主要体现在两个方面:
-
PathPattern只支持结尾部分使用**,如果在路径的中间使用**就会报错,上文中第一个和第三个接口,在 PathPattern 模式下会报错 -
PathPattern支持使用诸如{*path}的方式进行路径匹配,这种写法也可以匹配到多层路径,并且将匹配到的值赋值给 path 变量,例如如下一个接口
@RestController
public class PathPatternDemo {
@GetMapping("/path/{*path}")
public void hello6() {
System.out.println("测试");
}
}
如果请求路径是 http://localhost:8080/path/aa,那么参数 path 的值就是 /aa;
如果请求路径是 http://localhost:8080/path/aa/bb/cc/dd,那么参数 path 的值就是 /aa/bb/cc/dd;
这个写法也比较新颖,因为之前的 AntPathMatcher 里边没有这个。
我们知道/**和/{*pathVariable}都有匹配剩余所有path的“能力”,那它俩到底有什么区别呢?
-
/**能匹配成功,但无法获取到动态成功匹配元素的值 -
/{*pathVariable}可认为是/**的加强版:可以获取到这部分动态匹配成功的值
和**的优先级关系
既然/**和/{*pathVariable}都有匹配剩余path的能力,那么它俩若放在一起,优先级关系是怎样的呢?
@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里面而后放/**,但最后还是/**在前
结论:当二者同时出现(出现冲突)时,/**优先匹配
如何配置
默认情况下,SpringMVC 中使用的还是 AntPathMatcher,那么如何开启 PathPattern 呢?很简单,在 SpringBoot 项目中只需要添加如下配置即可:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setPatternParser(new PathPatternParser());
}
}
添加了这个配置后,在我们文章一开始贴出来的代码里,就会进入到 if 分支中,进而使用 PathPattern 去解析请求 URL。
但是这个配置有一个问题,在SpringBoot版本2.5.X与2.6.X上就算配置了也会报错,说没有切换到PathPattern,但是在2.4.X就没有问题。
PathPattern去掉了Ant字样,但保持了很好的向下兼容性:除了不支持将**写在path中间之外,其它的匹配规则从行为上均保持和AntPathMatcher一致,并且还新增了强大的{*pathVariable}的支持。
PathPattern语法更适合于web应用程序,非Web环境依旧有且仅有一种选择,那便是AntPathMatcher,因为PathPattern是专为Web环境设计,不能用于非Web环境。所以像上面资源加载、包名扫描之类的,底层依旧是交给AntPathMatcher去完成。
