1. 概述

Spring 5 引入了全新的 PathPatternParser 用于解析 URI 模板,这是对传统 AntPathMatcher 的升级。

✅ 核心改进点:

  • AntPathMatcher 采用 Ant 风格路径匹配
  • PathPatternParser 将路径拆分为 PathElements 链表结构
  • PathPattern 类基于此实现高效模式匹配
  • 新增 URI 变量语法支持

本文将系统讲解 Spring 5.0 WebFlux 中新增的 URL 模式匹配器,以及历史版本中延续的经典匹配规则。

2. Spring 5.0 新增 URL 模式匹配器

Spring 5.0 新增了极其便捷的 URI 变量语法:{*foo},可捕获路径末尾的任意数量路径段。

2.1 使用 Handler 方法实现 {*foo} 语法

通过 @GetMapping 和 Handler 方法演示 *id 模式:路径中 /spring5 之后的所有内容都会被存入 id 变量。

@GetMapping("/spring5/{*id}")
public String URIVariableHandler(@PathVariable String id) {
    return id;
}

测试用例验证多级路径捕获:

@Test
public void givenHandlerMethod_whenMultipleURIVariablePattern_then200() {
        
    client.get()
      .uri("/spring5/baeldung/tutorial")
      .exchange()
      .expectStatus()
      .is2xxSuccessful()
      .expectBody()
      .equals("/baeldung/tutorial");

    client.get()
      .uri("/spring5/baeldung")
      .exchange()
      .expectStatus()
      .is2xxSuccessful()
      .expectBody()
      .equals("/baeldung");
}

2.2 使用 RouterFunction 实现 {*foo} 语法

通过 RouterFunction 演示新语法:/test 后的完整路径会被捕获到 id 变量。

private RouterFunction<ServerResponse> routingFunction() {
    return route(GET("/test/{*id}"), 
      serverRequest -> ok().body(fromValue(serverRequest.pathVariable("id"))));
}

测试用例验证多级路径捕获:

@Test
public void givenRouter_whenMultipleURIVariablePattern_thenGotPathVariable() 
  throws Exception {
 
    client.get()
      .uri("/test/ab/cd")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("/ab/cd");
}

2.3 使用 {*foo} 语法访问资源

资源访问场景下,路径模式 /files/{*filepaths} 的匹配规则:

  • 路径 /files/hello.txtfilepaths = /hello.txt
  • 路径 /files/test/test.txtfilepaths = /test/test.txt

资源访问的 RouterFunction 实现:

private RouterFunction<ServerResponse> routingFunction() { 
    return RouterFunctions.resources(
      "/files/{*filepaths}", 
      new ClassPathResource("files/"))); 
}

测试用例验证资源访问(假设 hello.txt 内容为 "hello",test.txt 内容为 "test"):

@Test 
public void givenResources_whenAccess_thenGot() 
  throws Exception { 
      client.get() 
        .uri("/files/test/test.txt") 
        .exchange() 
        .expectStatus() 
        .isOk() 
        .expectBody(String.class) 
        .isEqualTo("test");
 
      client.get() 
        .uri("/files/hello.txt") 
        .exchange() 
        .expectStatus() 
        .isOk() 
        .expectBody(String.class) 
        .isEqualTo("hello"); 
}

3. 历史版本中的经典 URL 模式

以下模式在 Spring 历史版本中已支持,且同时适用于 RouterFunction@GetMapping

3.1 ? 匹配单个字符

模式 /t?st 的匹配规则:

  • ✅ 匹配:/test/tast
  • ❌ 不匹配:/tst/teest

RouterFunction 实现与测试:

private RouterFunction<ServerResponse> routingFunction() { 
    return route(GET("/t?st"), 
      serverRequest -> ok().body(fromValue("Path /t?st is accessed"))); 
}

@Test
public void givenRouter_whenGetPathWithSingleCharWildcard_thenGotPathPattern()   
  throws Exception {
 
      client.get()
        .uri("/test")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo("Path /t?st is accessed");
}

3.2 * 匹配路径段内 0 或多个字符

模式 /baeldung/*Id 的匹配示例:

  • ✅ 匹配:/baeldung/Id/baeldung/tutorialId/baeldung/articleId

RouterFunction 实现与测试:

private RouterFunction<ServerResponse> routingFunction() { 
    return route(
      GET("/baeldung/*Id"), 
      serverRequest -> ok().body(fromValue("/baeldung/*Id path was accessed"))); 
}

@Test
public void givenRouter_whenGetMultipleCharWildcard_thenGotPathPattern() 
  throws Exception {
      client.get()
        .uri("/baeldung/tutorialId")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo("/baeldung/*Id path was accessed");
}

3.3 ** 匹配 0 或多个完整路径段

模式 /resources/** 可匹配 /resources/ 后的任意路径:

private RouterFunction<ServerResponse> routingFunction() { 
    return RouterFunctions.resources(
      "/resources/**", 
      new ClassPathResource("resources/"))); 
}

@Test
public void givenRouter_whenAccess_thenGot() throws Exception {
    client.get()
      .uri("/resources/test/test.txt")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(String.class)
      .isEqualTo("content of file test.txt");
}

3.4 路径变量中使用正则 {baeldung:[a-z]+}

模式 /{baeldung:[a-z]+} 要求变量值匹配正则表达式:

private RouterFunction<ServerResponse> routingFunction() { 
    return route(GET("/{baeldung:[a-z]+}"), 
      serverRequest ->  ok()
        .body(fromValue("/{baeldung:[a-z]+} was accessed and "
        + "baeldung=" + serverRequest.pathVariable("baeldung")))); 
}

@Test
public void givenRouter_whenGetRegexInPathVarible_thenGotPathVariable() 
  throws Exception {
 
      client.get()
        .uri("/abcd")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo("/{baeldung:[a-z]+} was accessed and "
          + "baeldung=abcd");
}

3.5 单路径段多变量 {var1}_{var2}

⚠️ 注意:Spring 5 要求多变量必须用分隔符区分,否则无法正确解析:

private RouterFunction<ServerResponse> routingFunction() { 
 
    return route(
      GET("/{var1}_{var2}"),
      serverRequest -> ok()
        .body(fromValue( serverRequest.pathVariable("var1") + " , " 
        + serverRequest.pathVariable("var2"))));
 }

@Test
public void givenRouter_whenGetMultiplePathVaribleInSameSegment_thenGotPathVariables() 
  throws Exception {
      client.get()
        .uri("/baeldung_tutorial")
        .exchange()
        .expectStatus()
        .isOk()
        .expectBody(String.class)
        .isEqualTo("baeldung , tutorial");
}

4. 总结

本文系统梳理了 Spring 5 新增的 URL 匹配特性及历史版本中的经典模式。实际开发中踩坑提示:

  • 新语法 {*foo} 在路径捕获场景下非常高效
  • 正则匹配需注意性能开销
  • 多变量分隔符是必选项,避免解析错误

完整示例代码可参考 GitHub 仓库


原始标题:Spring MVC URL Matching Improvements | Baeldung