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