【SpringBoot WebFlux 系列】WebFlux 之 Path 參數解析與 url 映射

【SpringBoot WebFlux 系列】WebFlux 之 Path 參數解析與 url 映射

異步、反應式、函數式編程,近來可以說是逐漸主流了;Spring5 通過 Reactor 增加了對反應式編程的支持,而 Spring WebFlux 不同於以往的 web 框架,作爲一個非阻塞異步 web 框架,可以充分的利用多核 CPU 硬件資源,提供更強的併發支持;Spring 官方對 WebFlux 的支持非常友好,基本上對於慣於 Spring WEB 的 java 開發者,可以很簡單的遷移過來

接下來我們將進入 WebFlux 系列教程,努力使用最簡明的語言,來介紹一下 WebFlux 的基本玩法,讓各位小夥伴可以順暢的切換和使用 WebFlux 來體驗反應式編程的魅力

本文將主要介紹 WebFlux 提供 web 接口時的 url 匹配,以及對應的 path 參數解析

I. 項目環境

本項目藉助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA進行開發

1. 依賴

使用 WebFlux,最主要的引入依賴如下(省略掉了 SpringBoot 的相關依賴,如對於如何創建 SpringBoot 項目不太清楚的小夥伴,可以關注一下我之前的博文)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
</dependencies>

II. Path 匹配與參數解析

下面所有內容基於官方文檔完成: https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-ann-requestmapping-uri-templates

下面的示例主要是基於註解的方式,基本知識點和 SpringWeb 沒有太大的區別(至於函數式的用法,後面會專門介紹)

1. 基本 path 參數獲取

path 參數,舉例如: http://127.0.0.1:8080/name/testnametest就算是 path 參數,我們主要是藉助@PathVariable來獲取

一個具體實例

@RestController
@RequestMapping(path = "path")
public class PathAction {

    /**
     * 最基本的path獲取方式
     *
     * @param index
     * @return
     */
    @GetMapping(path = "/basic/{index}")
    public Mono<String> basic(@PathVariable(name = "index") int index) {
        return Mono.just("path index: " + index);
    }
}

針對上面的 case,我們簡單的設計了三個訪問 case,具體結果如下

➜  ~ curl 'http://127.0.0.1:8080/path/basic/1'
path index: 1%

➜  ~ curl 'http://127.0.0.1:8080/path/basic/1/2'
{"timestamp":"2020-08-26T13:35:26.221+0000","path":"/path/basic/1/2","status":404,"error":"Not Found","message":null,"requestId":"8256bf73"}%

➜  ~ curl 'http://127.0.0.1:8080/path/basic/'
{"timestamp":"2020-08-26T13:35:32.196+0000","path":"/path/basic/","status":404,"error":"Not Found","message":null,"requestId":"eeda1111"}%

請注意上面的輸出,/basic/{index} 只能匹配單級的 path 路徑參數,而且上面的寫法中,這級 path 路徑必須存在

查看PathVariable註解可以看到裏面有一個required屬性,如果設置爲 false,會怎樣呢

@GetMapping(path = "/basic2/{index}")
public Mono<String> basic2(@PathVariable(name = "index", required = false) Integer index) {
    return Mono.just("basic2 index: " + index);
}

測試 case 如下

➜  ~ curl 'http://127.0.0.1:8080/path/basic2/'
{"timestamp":"2020-08-26T13:41:40.100+0000","path":"/path/basic2/","status":404,"error":"Not Found","message":null,"requestId":"b2729e2c"}%

➜  ~ curl 'http://127.0.0.1:8080/path/basic2/22'
basic2 index: 22%


➜  ~ curl 'http://127.0.0.1:8080/path/basic2/22/3'
{"timestamp":"2020-08-26T13:41:44.400+0000","path":"/path/basic2/22/3","status":404,"error":"Not Found","message":null,"requestId":"0b3f173c"}%

從上面的實際 case,也可以看出來,級別這個屬性設置爲 false,但是 url 路徑依然需要正確匹配,多一級和少一級都不行

2. 多 path 參數

上面只有一個 path 參數,如果有多個參數,也比較簡單

/**
 * 多個參數的場景
 *
 * @param index
 * @param order
 * @return
 */
@GetMapping(path = "/mbasic/{index}/{order}")
public Mono<String> mbasic(@PathVariable(name = "index") int index, @PathVariable(name = "order") String order) {
    return Mono.just("mpath arguments: " + index + " | " + order);
}

測試 case 如下

➜  ~ curl 'http://127.0.0.1:8080/path/mbasic/1/asc'
mpath arguments: 1 | asc%

3. 部分 path 參數匹配

上面的兩個 case,都是完整的匹配某一級路徑,下面介紹部分匹配的 case

/**
 * 路徑中的部分內容匹配
 *
 * - /part/test.txt -> name = test
 * - /part/a/test.txt -> 不匹配
 *
 * @param name
 * @return
 */
@GetMapping(path = "/part/{name}.txt")
public Mono<String> part(@PathVariable(name = "name") String name) {
    return Mono.just("part path argument: " + name);
}

請注意上面的 path 路徑,後綴是.txt,如下面的實例中part/hello.txt中那麼對應的就是hello

➜  ~ curl 'http://127.0.0.1:8080/path/part/hello.txt'
part path argument: hello%

➜  ~ curl 'http://127.0.0.1:8080/path/part/hello.tx'
{"timestamp":"2020-08-26T13:47:49.121+0000","path":"/path/part/hello.tx","status":404,"error":"Not Found","message":null,"requestId":"1075d683"}%

4. 正則匹配

接下來更高端的 path 參數匹配來了,支持一些簡單的正則,如我們希望對spring-web-3.0.5.jar這段 path 路徑進行解析,希望將spring-web作爲name, 3.0.5作爲version.jar作爲ext

因此我們的 rest 接口寫法可以如下

/**
 * 正則匹配
 *
 * /path/path/pattern/spring-web-3.0.5.jar  -> name = spring-web,  version=3.0.5,  ext=.jar
 *
 * @return
 */
@GetMapping(path = "/pattern/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public Mono<String> urlPattern(@PathVariable(name = "name") String name,
        @PathVariable(name = "version") String version, @PathVariable(name = "ext") String ext) {
    return Mono.just("pattern arguments name=" + name + " version=" + version + " ext=" + ext);
}

5. 多級 path 參數匹配

注意上面的所有寫法,都有一個特點,那就是隻能針對單級的 path 路徑進行全/部分匹配(本文中將 path 路徑中//之間作爲一級),那麼如果我希望我的 path 參數可以匹配多級,可以怎麼辦

  • /path/name/hello 請求路徑中,我希望將 /name/hello 作爲一個 path 參數

針對上面的場景,我們主要是藉助{*name}方式來處理,注意這個參數名前面的*號

/**
 * 匹配:
 *
 * - /path/pattern2  -> name == ""
 * - /path/pattern2/hello  -> name == /hello
 * - /path/pattern2/test/hello -> name = /test/hello
 *
 * @param name
 * @return
 */
@GetMapping(path = "/pattern2/{*name}")
public Mono<String> pattern2(@PathVariable(name = "name") String name) {
    return Mono.just("pattern2 argument: " + name);
}

測試 case 如下

➜  ~ curl 'http://127.0.0.1:8080/path/pattern2'
pattern2 argument: %

➜  ~ curl 'http://127.0.0.1:8080/path/pattern2/hello'
pattern2 argument: /hello%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern2/hello/world'
pattern2 argument: /hello/world%

6. 路徑匹配

前面介紹的是 path 參數解析,接下來我們簡單的看一下最常見的三種路徑匹配方式

a. *

一個星號,表示匹配 0 個 or1 個單級 path 路徑

/**
 * 單個*號,只能匹配一級目錄,注意這種方式與上面的 pattern2 之間的區別
 *
 * 可以匹配:
 *
 * - /path/pattern3/hello
 * - /path/pattern3
 *
 * 不能匹配
 *
 * - /path/pattern3/hello/1
 *
 * @return
 */
@GetMapping(path = "/pattern3/*")
public Mono<String> pattern3() {
    return Mono.just("pattern3 succeed!");
}

實測 case 如下

# 請注意,這裏是沒有/結尾的
➜  ~ curl 'http://127.0.0.1:8080/path/pattern3'
{"timestamp":"2020-08-27T00:01:20.703+0000","path":"/path/pattern3","status":404,"error":"Not Found","message":null,"requestId":"c88f5066"}%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern3/'
pattern3 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern3/a'
pattern3 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern3/a/b'
{"timestamp":"2020-08-27T00:01:18.144+0000","path":"/path/pattern3/a/b","status":404,"error":"Not Found","message":null,"requestId":"203dc7d4"}%

請注意上面的實例,/path/pattern3 訪問 404, 而/path/pattern3/是可以的,唯一的區別就是多了一個後綴/

  • why?
  • 是因爲 path 路徑的星號前面有一個/導致的麼?

接下來我們再設計一個 case,將*前面的/幹掉,再測試一下

@GetMapping(path = "/pattern33**")
public Mono<String> pattern33() {
    return Mono.just("pattern33 succeed!");
}

再次測試,結果如下

➜  ~ curl 'http://127.0.0.1:8080/path/pattern3311'
pattern33 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern33/11'
{"timestamp":"2020-08-27T00:05:51.236+0000","path":"/path/pattern33/11","status":404,"error":"Not Found","message":null,"requestId":"d8cbd546"}%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern33'
pattern33 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern331/'
pattern33 succeed!%

藉助前面兩個 case,我們基本上可以看出*的作用

  • *前面的完全匹配
    • 比如/pattern3/*,那麼訪問的 path 路徑前綴必須是/pattern3/
  • *最多表示單級路徑,簡單來講就是*所代表的的位置中不能出現/x
    • 比如/pattern33**,那麼/pattern331/可以匹配,但是/pattern331/1不能

b. **

有別與上面的單個*匹配 0-1 級 path 路徑,兩個**則表示可以一直匹配到最後一層

/**
 * 對於 pattern4開頭的都可以匹配
 *
 * @return
 */
@GetMapping(path = "/pattern4/**")
public Mono<String> pattern4() {
    return Mono.just("pattern4 succeed!");
}

測試 case 如下

➜  ~ curl 'http://127.0.0.1:8080/path/pattern4'
pattern4 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern4/12'
pattern4 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern4/12/3'
pattern4 succeed!%

請注意

  • 直接訪問/pattern4也是可以命中的,這個和上面是有區別的

c. ?

單個字符的通配,比較簡單如下

/**
 * 匹配  pattern5/test   pattern5/tast ...
 * 不匹配 pattern5/tst pattern5/tesst
 *
 * @return
 */
@GetMapping(path = "/pattern5/t?st")
public Mono<String> pattern5() {
    return Mono.just("pattern5 succeed!");
}

訪問 case

➜  ~ curl 'http://127.0.0.1:8080/path/pattern5/test'
pattern5 succeed!%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern5/t/st'
{"timestamp":"2020-08-27T00:13:42.557+0000","path":"/path/pattern5/t/st","status":404,"error":"Not Found","message":null,"requestId":"add34639"}%

➜  ~ curl 'http://127.0.0.1:8080/path/pattern5/tst'
{"timestamp":"2020-08-27T00:14:01.078+0000","path":"/path/pattern5/tst","status":404,"error":"Not Found","message":null,"requestId":"b2691121"}%

從上面的測試輸出也可以看出

  • ? 對應的地方不能是/以及其他不被支持的字符(如?,',", %等)
  • ? 對應的地方必須存在

7. 小結

雖然本文的主題是 webflux 中 path 參數解析與 url 映射匹配,但是看下來我們會神奇的發現,這些知識點和 SpringMVC 中,貌似也沒有什麼區別,事實上也確實如此;對於註解的使用場景時,絕大多數,都是之前怎麼玩,現在依然可以怎麼玩

下面用一個表格針對上面的知識點進行彙總

pattern 描述 舉例
? 匹配一個字符 pages/t?st.html 匹配 /pages/test.html and /pages/t3st.html
* 匹配單級 path 路徑中 0-多個字符 "/resources/*.png" matches "/resources/file.png"
"/projects/*/versions" matches "/projects/spring/versions" but does not match "/projects/spring/boot/versions"
** 匹配 0-多個 path 路徑 "/resources/**" matches "/resources/file.png" and "/resources/images/file.png"
"/resources/**/file.png"這種寫法是非法的
{name} 匹配單級 path 路徑參數 "/projects/{project}/versions" matches "/projects/spring/versions" and captures project=spring
{name:[a-z]+} 正則 "/projects/{project:[a-z]+}/versions" matches "/projects/spring/versions" but not "/projects/spring1/versions"
{*path} 匹配 path 路徑中,0-最後一級 path 路徑參數 "/resources/{*file}" matches "/resources/images/file.png" and captures file=images/file.png

II. 其他

0. 項目

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

一灰灰blog

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章