JAX-RS 的核心功能是處理向 URI 發送的請求,所以它提供了一些匹配模式以便簡化對 URI 的解析。樓主在本系列的上一篇文章中已經使用了最簡單的路徑參數,本文將介紹一些稍微高級點的咚咚。
模板參數
前面已經見過用 @Path("{id}")
和 @PathParam("id")
來匹配路徑參數 id
。這種匹配方式可以被嵌入到 @Path
注解中的任何地方,從而匹配多個參數,例如下面的代碼用來查找 ID 在某一范圍內的電影:
@GET
@Path("{min}~{max}")
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
于是,GET /ms/rest/movie/5~16
就將返回 ID 為 5 到 16 的電影。此處的 min
和 max
已被自動轉換為 int
類型。JAX-RS 支持多種類型的自動轉換,詳見 @PathParam
的文檔。
根據 HTTP 規范,參數可能會編碼。默認情況下,JAX-RS 會自動解碼。如果希望得到未解碼的參數,只需在參數上再加個 @Encoded
注解。該注解適用于大多數 JAX-RS 注入類型,但并不常用。
模板參數雖然靈活,也可能會帶來歧義。例如想用 {firstName}-{lastName}
匹配一個人的姓名,但恰好某人的名(lastName
)含有“-”字符,像 O-live K 這種,匹配后就會變成姓 live-K,名 O。這種場景很難避免,一種簡單的解決方法就是對參數值進行兩次編碼,然后在服務端代碼解碼一次,因為 JAX-RS 默認會進行一次解碼,或者加上 @Encoded
注解,自己進行兩次解碼。
另外,在一個復雜系統中,多個 @Path
可能會造成路徑混淆,例如 {a}-
和 {a}-z
都能匹配路徑 a-z
。雖然 JAX-RS 定義了一些規則來指定匹配的優先級,但這些規則本身就比較復雜,并且也不能完全消除混淆。樓主認為,設計一個 REST 系統的核心就是對 URI 的設計,應當小心處理 URI 的結構,合理分類,盡量保證匹配的唯一性,而不要過度使用晦澀的優先級規則。樓主將在下一篇文章介紹優先級規則。
正則表達式
模板參數可以用一個正則表達式進行驗證,寫法是在模板參數的標識符后面加一個冒號,然后跟上正則表達式字符串。例如在根據 ID 查詢電影信息的代碼中,模板參數 {id}
只能是整數,于是代碼可以改進為:
@GET
@Path("{id : \\d+}")
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
冒號左右的空格將被忽略。用正則表達式驗證數據很有局限性,可惜 JAX-RS 目前并不能直接集成 Bean 驗證框架,因此復雜的驗證只能靠自己寫代碼。
查詢參數
查詢參數很常見,就是在 URI 的末尾跟上一個問號和一系列由“&”分隔的鍵值對,例如查詢 ID 為 5 到 16 的電影也可以設計為 /ms/rest/movie?min=5&max=16
。JAX-RS 提供了 QueryParam
來注入查詢參數:
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> findMovies(@DefaultValue("0") @QueryParam("min") int min,
@DefaultValue("0") @QueryParam("max") int max) {
查詢參數是可選的。如果 URI 沒有設定某個查詢參數,JAX-RS 就會根據情況為其生成 0、空字符串之類的默認值。如果要手動設定默認值,需要像上面的代碼一樣用 @DefaultValue
注解來指定。另外還可以加上 Encoded
注解來得到編碼的原始參數。
有的查詢參數是一對多的鍵值對,例如 /xyz?a=def&a=pqr
,這種情況只需將注入的參數類型改為 List
即可。
矩陣參數
矩陣參數應該屬于 URI 規范中的非主流類型,但它實際上比查詢參數更靈活,因為它可以嵌入到 URI 路徑中的任何一段末尾(用分號隔開),用來標識該段的某些屬性。例如 GET /ms/rest/movie;year=2011/title;initial=A
表示在 2011 年出品的電影中查找首字母為 A 的標題。year
是電影的屬性,而 initial
是標題的屬性,這比把它們都作為查詢參數放在末尾更直觀可讀。匹配 URI 的時候,矩陣參數將被忽略,因此前面的 URI 匹配為 /ms/rest/movie/title
。矩陣參數可以用 @MatrixParam
來注入:
@GET
@Path("title")
@Produces(MediaType.APPLICATION_JSON)
public List<String> findTitles(@MatrixParam("year") int year,
@MatrixParam("initial") String initial) {
如果 URI 的多個段中含有相同名稱的矩陣參數,例如 /abc;name=XXX/xyz;name=OOO
,這種直接注入就失效了,只能用下面要講的編程式訪問來取得。
編程式訪問
如果簡單的注入不能達到目的,就需要通過注入 PathSegment
或 UriInfo
對象來直接編程訪問 URI 的信息。
一個 PathSegment
對象代表 URI 中的一個路徑段,可以從它得到矩陣參數。它可以通過 @PathParam
來注入,這要求該路徑段必須整個被定義為一個模板參數。例如下面的代碼也可以用來處理 GET /ms/rest/movie/{id}
:
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Movie findMovie(@PathParam("id") PathSegment ps) {
@PathParam
也可以注入多個段,如果想把 /a/b/c/d
匹配到 /a/{segments}/d
,直接注入一個字符串顯然不行,因為 b/c
是兩個路徑段。唯一的選擇是把注入的類型改為 List<PathSegment>
。樓主嚴重不推薦用一個模板參數匹配多個路徑段,因為這很容易干擾其他匹配的設計,最后搞成一團亂麻。URI 路徑段應當盡量設計得簡單明晰,再輔以矩陣參數或查詢參數就能應付大多數場景。不論對服務端還是客戶端開發人員來說,簡潔的 URI 既便于管理,又便于使用。網上有不少關于 URI 設計指南的文章,此處不再贅述。
如果想完全手動解析路徑,則可以用 @Context
注入一個 UriInfo
對象,通過此對象可以得到 URI 的全部信息,詳見 API 文檔。例如:
@GET
@Path("{id}/{segments}")
@Produces(MediaType.PLAIN_TEXT)
public String getInfo(@PathParam("id") int id, @Context UriInfo uriInfo) {
UriInfo
主要用在某些特殊場合下起輔助作用,設計良好的 URI 用普通的注入就能完成大部分匹配。
工欲善其事必先利其器,為此 JAX-RS 提供了這些利器來解析 URI。至于如何用這些器來做出一個好系統,則還是依賴于 URI 本身的設計。