前面介紹了各種請(qǐng)求參數(shù)的注入,這些參數(shù)在 HTTP 請(qǐng)求中都是以純文本的方式存在的。在處理參數(shù)的時(shí)候,往往需要把這些文本參數(shù)轉(zhuǎn)換為 Java 對(duì)象。JAX-RS 提供了一些內(nèi)置的規(guī)則里自動(dòng)完成這種轉(zhuǎn)換。
JAX-RS 提供了四條自動(dòng)類型轉(zhuǎn)換規(guī)則,下面我們逐條考察。
這個(gè)早就見識(shí)過了,無需多說。舉例回顧一下:
@GET @Path("{id}") public Movie getXxx(@PathParam("id") int id) {/*...*/}
String
參數(shù)的構(gòu)造器的類型這個(gè)也不難理解,JAX-RS 會(huì)自動(dòng)調(diào)用該構(gòu)造器創(chuàng)建一個(gè)對(duì)象:
public class Style { public Style(String name) {/* ... */} // ... } @GET @Path("{name}") public Movie getXxx(@PathParam("name") Style style) { // JAX-RS 已自動(dòng)調(diào)用 xxx = new Style(name) // ... }
valueOf(String)
的類型也好理解。特別需要注意的是,所有的枚舉類型都在此列,因?yàn)榫幾g器會(huì)自動(dòng)給枚舉類型加上一個(gè)這樣的工廠方法。例如:
public enum Style {/*...*/} @GET @Path("{name}") public Movie getXxx(@PathParam("name") Style style) { // JAX-RS 已自動(dòng)調(diào)用 style = Style.valueOf(name) // ... }
List<T>
、Set<T>
和 SortedSet<T>
這條規(guī)則適用于多值參數(shù),例如查詢參數(shù):
@GET @Path("xxx") public Movie getXxx(@QueryParam("style") Set<Style> styles) { // JAX-RS 已自動(dòng)轉(zhuǎn)換每個(gè) Style 對(duì)象并組裝到 Set 中 // ... }
如果轉(zhuǎn)換失敗,JAX-RS 會(huì)根據(jù)情況自動(dòng)拋出一個(gè)包裝了初始異常,但是帶不同 HTTP 錯(cuò)誤碼的 WebApplicationException
:對(duì)矩陣參數(shù)(@MatrixParam
)、查詢參數(shù) (@QueryParam
)或路徑參數(shù)(@PathParam
)來說為 HTTP 404 找不到
,而對(duì)頭部參數(shù)(@HeaderParam
)或 Cookie 參數(shù)(@CookieParam
)為 HTTP 400 錯(cuò)誤請(qǐng)求
。
在《JAX-RS 從傻逼到牛叉 3:路徑匹配》中,我們已經(jīng)見過如何使用 @PathParam
、@QueryParam
和 @MatrixParam
分別注入 URI 中的路徑參數(shù)、矩陣參數(shù)和查詢參數(shù),以及如何編程訪問這些參數(shù)。本文介紹表單參數(shù)、HTTP 頭部參數(shù)和 Cookie 參數(shù)的注入。
HTTP 請(qǐng)求也可以使用提交表單的方式。這時(shí)請(qǐng)求方法一般是 POST,當(dāng)然春哥也無法阻止你用 GET。在前面我們雖然介紹過處理 POST 請(qǐng)求的例子,但那只是利用了 JAX-RS 對(duì) JAXB 的支持,并沒有涉及到對(duì)具體請(qǐng)求參數(shù)的注入。JAX-RS 提供了 @FormParam
注解來注入 POST 請(qǐng)求的參數(shù),例如:
@POST public Response createMovie(@FormParam("title") String title) { // 此處省略若干行 }
這兒省略了 @Consumes
注解,JAX-RS 會(huì)自動(dòng)默認(rèn)為 @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
,也就是 application/x-www-form-urlencoded
格式的請(qǐng)求。如果請(qǐng)求格式為 multipart/form-data
,就必須顯示指明:
@POST @Consumes(MediaType.MULTIPART_FORM_DATA) public Response createMovie(@FormParam("title") String title) { // 此處省略若干行 }
JAX-RS 還支持文件的上傳和下載,以后再介紹。
注入 HTTP 頭部參數(shù)簡(jiǎn)單得不能再簡(jiǎn)單了:
@GET @Path("xxx") @Produces(MediaType.TEXT_PLAIN) public String xxx(@HeaderParam("User-Agent") String userAgent) { // 此處省略若干行 }
如果有很多頭部參數(shù),為了避免臃腫的參數(shù)列表,可以注入一個(gè)頭部對(duì)象,然后編程訪問頭部參數(shù):
@GET @Path("xxx") @Produces(MediaType.TEXT_PLAIN) public String xxx(@Context HttpHeaders headers) { // 此處省略若干行 }
注入 Cookie 參數(shù)同樣的簡(jiǎn)單:
@GET @Path("xxx") @Produces(MediaType.TEXT_PLAIN) public String xxx(@CookieParam("userName") String userName) { // 此處省略若干行 }
如果希望編程訪問,則可以像編程訪問那樣注入一個(gè) HttpHeaders
對(duì)象,然后通過它的 getCookies()
方法來獲取所有的 Cookie。
目前我們的電影服務(wù)只提供了對(duì)電影信息的訪問服務(wù),現(xiàn)在我們要再增加兩項(xiàng)級(jí)服務(wù),分別用來訪問導(dǎo)演和演員信息。加上原先的電信信息服務(wù),我們把 URI 統(tǒng)一放到 /ms/rest/service/
的子路徑下。最先想到的方法就是為這三個(gè) URI 分別寫 JAX-RS 服務(wù):
@Singleton @Path("service/movie") public class MovieService { // 此處省略若干行 } @Singleton @Path("service/director") public class DirectorService { // 此處省略若干行 } @Singleton @Path("service/director") public class ActorService { // 此處省略若干行 }
這種寫法的缺點(diǎn)就是讓三個(gè)本來有點(diǎn)關(guān)系(父級(jí) URI 相同)的服務(wù)被放到了毫不相干的三個(gè)類里面,不一個(gè)個(gè)類地查看注解難以看出這點(diǎn)關(guān)系。為此,JAX-RS 提供了動(dòng)態(tài)資源綁定的功能,讓我們能夠?qū)@種情況做一些整理。
首先,我們引入一個(gè)服務(wù)定位器來處理集中管理這三個(gè)子級(jí)服務(wù):
@Singleton @Path("service") public class ServiceLocator { @Inject private MovieService movieService; @Inject private DirectorService directorService; @Inject private ActorService actorService; private Map<String, Object> serviceMap; @PostConstruct private initServiceMap() { serviceMap = new HashMap<>(); serviceMap.put("movie", movieService); serviceMap.put("director", directorService); serviceMap.put("actor", actorService); } @Path("{name}") public Object locateService(@PathParam("name") String name) { Object service = serviceMap.get(name); if (service == null) { throw new WebApplicationException(Status.SERVICE_UNAVAILABLE); } return service; } }
該類中的 locateService
方法根據(jù)服務(wù)的名稱返回相應(yīng)的服務(wù)實(shí)例,注意該方法只有一個(gè) @Path
注解,因?yàn)樗⒉磺宄?qǐng)求的具體內(nèi)容;返回對(duì)象的類型為 Object
,表明動(dòng)態(tài)資源定位不要求服務(wù)類實(shí)現(xiàn)相同的接口,只需要它們的方法帶有相應(yīng)的 JAX-RS 注解,就能夠被 JAX-RS 自動(dòng)發(fā)現(xiàn)和處理(專業(yè)術(shù)語稱為 introspect,內(nèi)省),以 MovieService
為例:
@Singleton public class MovieService { @GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public Movie find(@PathParam("id") int id) { Movie movie = movieDao.get(id); if (movie != null) { return movie; } else { throw new WebApplicationException(Status.NOT_FOUND); } } // 此處省略若干行 }
這樣,每個(gè)請(qǐng)求實(shí)際上都由兩個(gè)類先后處理。例如,處理請(qǐng)求 GET /ms/rest/service/movie/1
的時(shí)候,先由 ServiceLocator
返回相配的服務(wù)實(shí)例 movieService
,然后再由該實(shí)例的 find
方法返回結(jié)果。比起最開始那三個(gè)簡(jiǎn)單的類,雖然多了一層調(diào)用,但換來了更加清晰的結(jié)構(gòu)。
動(dòng)態(tài)資源定位是一個(gè)非常靈活強(qiáng)大的功能,用好的話,完全可以把 URI 層次整理成一個(gè)類似于文件目錄結(jié)構(gòu)的抽象文件系統(tǒng)。
籠子大了什么鳥都有。同樣的道理,不論多么細(xì)心地設(shè)計(jì) URI 結(jié)構(gòu),在系統(tǒng)復(fù)雜到一定程度后,仍然難以避免路徑?jīng)_突。為此,JAX-RS 使用一些規(guī)則來定義路徑匹配的優(yōu)先級(jí)。
如果某個(gè)請(qǐng)求路徑可以對(duì)上多個(gè) URI 匹配模式,那么 JAX-RS 就把可能匹配上的 URI 模式先拼接完整,按照下列規(guī)則依次進(jìn)行比較,直到找出最適合的匹配模式:
/
和模板參數(shù)。例如 /ms/rest/movie/{id : \\d+}
包含 11 個(gè)字面字符。/ms/rest/movie/{id : \\d+}
帶一個(gè)模板參數(shù)。/ms/rest/movie/{id : \\d+}
帶一個(gè)含正則表達(dá)式的模板參數(shù)。現(xiàn)在看一個(gè)例子。回顧一下,/ms/rest/movie/{id : \\d+}
已經(jīng)用來根據(jù) ID 獲取電影信息。為了制造麻煩,現(xiàn)在引入 /ms/rest/movie/{title}
來根據(jù)電影標(biāo)題獲取電影信息。先請(qǐng)你猜一猜 /ms/rest/movie/300
代表啥?ID 為 300 的神秘電影,還是我們可愛的勇士?只能跟著規(guī)則一條一條地看:
/ms/rest/movie/{id : \\d+}
帶了一個(gè)含正則表達(dá)式的模板參數(shù),勝利!所以返回 ID 為 300 的片片。傳說這三條規(guī)則能夠覆蓋 90% 以上的情景。不過我們馬上就能造出一個(gè)打破規(guī)則的東西:/ms/rest/movie/{title : [ \\w]+}
。經(jīng)過測(cè)試,/ms/rest/movie/300
會(huì)匹配上 /ms/rest/movie/{id : \\d+}
。如何解釋?JAX-RS 規(guī)范文檔 3.7.2 定義了完整的匹配規(guī)則,對(duì)于這兩個(gè)簡(jiǎn)單的 URI 匹配模式,似乎一直進(jìn)行到底都無法比較出優(yōu)先級(jí)。莫非有另外的潛規(guī)則?或者是 JAX-RS 的實(shí)現(xiàn)(參考實(shí)現(xiàn)為 Jersey)自行規(guī)定?但無論如何,搞出這種怪物本身就是一個(gè)設(shè)計(jì)錯(cuò)誤,所以也不必去深究原因。
JAX-RS 的核心功能是處理向 URI 發(fā)送的請(qǐng)求,所以它提供了一些匹配模式以便簡(jiǎn)化對(duì) URI 的解析。樓主在本系列的上一篇文章中已經(jīng)使用了最簡(jiǎn)單的路徑參數(shù),本文將介紹一些稍微高級(jí)點(diǎn)的咚咚。
前面已經(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。這種場(chǎng)景很難避免,一種簡(jiǎn)單的解決方法就是對(duì)參數(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)先級(jí),但這些規(guī)則本身就比較復(fù)雜,并且也不能完全消除混淆。樓主認(rèn)為,設(shè)計(jì)一個(gè) REST 系統(tǒng)的核心就是對(duì) URI 的設(shè)計(jì),應(yīng)當(dāng)小心處理 URI 的結(jié)構(gòu),合理分類,盡量保證匹配的唯一性,而不要過度使用晦澀的優(yōu)先級(jí)規(guī)則。樓主將在下一篇文章介紹優(yōu)先級(jí)規(guī)則。
模板參數(shù)可以用一個(gè)正則表達(dá)式進(jìn)行驗(yàn)證,寫法是在模板參數(shù)的標(biāo)識(shí)符后面加一個(gè)冒號(hào),然后跟上正則表達(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) {
冒號(hào)左右的空格將被忽略。用正則表達(dá)式驗(yàn)證數(shù)據(jù)很有局限性,可惜 JAX-RS 目前并不能直接集成 Bean 驗(yàn)證框架,因此復(fù)雜的驗(yàn)證只能靠自己寫代碼。
查詢參數(shù)很常見,就是在 URI 的末尾跟上一個(gè)問號(hào)和一系列由“&”分隔的鍵值對(duì),例如查詢 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ù)是一對(duì)多的鍵值對(duì),例如 /xyz?a=def&a=pqr
,這種情況只需將注入的參數(shù)類型改為 List
即可。
矩陣參數(shù)應(yīng)該屬于 URI 規(guī)范中的非主流類型,但它實(shí)際上比查詢參數(shù)更靈活,因?yàn)樗梢郧度氲?URI 路徑中的任何一段末尾(用分號(hào)隔開),用來標(biāo)識(shí)該段的某些屬性。例如 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
,這種直接注入就失效了,只能用下面要講的編程式訪問來取得。
如果簡(jiǎn)單的注入不能達(dá)到目的,就需要通過注入 PathSegment
或 UriInfo
對(duì)象來直接編程訪問 URI 的信息。
一個(gè) PathSegment
對(duì)象代表 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ì)得簡(jiǎn)單明晰,再輔以矩陣參數(shù)或查詢參數(shù)就能應(yīng)付大多數(shù)場(chǎng)景。不論對(duì)服務(wù)端還是客戶端開發(fā)人員來說,簡(jiǎn)潔的 URI 既便于管理,又便于使用。網(wǎng)上有不少關(guān)于 URI 設(shè)計(jì)指南的文章,此處不再贅述。
如果想完全手動(dòng)解析路徑,則可以用 @Context
注入一個(gè) UriInfo
對(duì)象,通過此對(duì)象可以得到 URI 的全部信息,詳見 API 文檔。例如:
@GET @Path("{id}/{segments}") @Produces(MediaType.PLAIN_TEXT) public String getInfo(@PathParam("id") int id, @Context UriInfo uriInfo) {
UriInfo
主要用在某些特殊場(chǎng)合下起輔助作用,設(shè)計(jì)良好的 URI 用普通的注入就能完成大部分匹配。
工欲善其事必先利其器,為此 JAX-RS 提供了這些利器來解析 URI。至于如何用這些器來做出一個(gè)好系統(tǒng),則還是依賴于 URI 本身的設(shè)計(jì)。
JAX-RS 使用注解進(jìn)行配置,所以用它開發(fā) REST 風(fēng)格的服務(wù)非常簡(jiǎn)單。樓主在本文用一個(gè)小例子來說明 JAX-RS 的基本用法。
假設(shè)樓主要開發(fā)一個(gè)小電影服務(wù),客戶端可以通過請(qǐng)求 URI 對(duì)電影進(jìn)行 CRUD 操作。為簡(jiǎn)明起見,這兒不使用數(shù)據(jù)庫,只在內(nèi)存中模擬。先用一個(gè)非常簡(jiǎn)單的 Movie
類,在后續(xù)的文章中根據(jù)情況逐步擴(kuò)充:
public class Movie { private int id; private String title; // 此處省略若干行 }
嗯,就是一個(gè)很普通的 JavaBean,實(shí)際項(xiàng)目中可以根據(jù)需要加上 @Entity
等注解。接下來看看如何編寫 JAX-RS 服務(wù)。
一個(gè) JAX-RS 服務(wù)就是一個(gè)使用了 JAX-RS 注解來將 HTTP 請(qǐng)求綁定到方法的 Java 類,一共支持兩種類型:?jiǎn)握?qǐng)求對(duì)象或單例對(duì)象。單請(qǐng)求對(duì)象意味著每來一個(gè)請(qǐng)求,就創(chuàng)建一個(gè)服務(wù)對(duì)象,在請(qǐng)求結(jié)束時(shí)銷毀。單例對(duì)象則意味著只有一個(gè)服務(wù)對(duì)象處理所有的請(qǐng)求,從而可以在多個(gè)請(qǐng)求間維持服務(wù)狀態(tài)。JAX-RS 服務(wù)可通過繼承 javax.ws.rs.core.Application
來定義,其中的 getClasses
方法返回單請(qǐng)求對(duì)象的類型,getSingletons
方法返回單例對(duì)象的類型。這兩個(gè)方法是可選的。在 Java EE 6 環(huán)境中,如果這兩個(gè)方法都返回 null
或者空集合,那么應(yīng)用程序中的所有 JAX-RS 都將被部署。這時(shí)可以用 CDI 的 @javax.inject.Singleton
或者 EJB 的 @javax.ejb.Singleton
注解來指定單例對(duì)象。
如果電影服務(wù)的上下文根路徑為 http://localhost/ms,而樓主希望將服務(wù)部署到 http://localhost/ms/rest 下面,只需要寫一個(gè)類:
@ApplicationPath("rest") public class RestApplication extends Application { }
@ApplicationPath
注解指定所有服務(wù)的相對(duì)基址,如果為空字符串,則直接使用上下文根路徑。另一種配置方式是在 web.xml 文件中進(jìn)行聲明,那是為了使 JAX-RS 能在 Servlet 容器(例如 Tomcat)中運(yùn)行,此處略過。這項(xiàng)配置必不可少,否則無法部署服務(wù)。
很好很強(qiáng)大,現(xiàn)在開始編寫電影服務(wù)類 MovieService
,先看看聲明和初始化:
@Singleton @Path("movie") public class MovieService { private AtomicInteger ai; private ConcurrentMap<Integer, Movie> movieMap; @PostConstruct private void init() { ai = new AtomicInteger(); movieMap = new ConcurrentHashMap<>(); int id = ai.getAndIncrement(); movieMap.put(id, new Movie().setId(id).setTitle("Avatar")); }
因?yàn)闃侵髦恍枰粋€(gè)“內(nèi)存數(shù)據(jù)庫”,所以用單例對(duì)象即可,此處使用 CDI 的 @javax.inject.Singleton
來聲明單例。@Path
聲明了一個(gè)服務(wù),它指示 MovieService
負(fù)責(zé)處理發(fā)送到 http://localhost/ms/rest/movie 的請(qǐng)求。路徑的拼接方式非常直觀。init
方法帶有 @PostConstruct
注解,因此將在 MovieService
構(gòu)造完成后立即調(diào)用,它向 movieMap
中存入了一個(gè) ID 為 0 的 Movie
對(duì)象。為簡(jiǎn)化代碼,Movie
的設(shè)置方法都返回 this
,有點(diǎn)偽造構(gòu)建者模式的味道。
接下來看看如何處理 HTTP 請(qǐng)求。
GET 請(qǐng)求用于獲取一個(gè)或多個(gè)資源。在本例中用來獲取一部電影的信息:
@GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public Movie find(@PathParam("id") int id) { Movie movie = movieMap.get(id); if (movie != null) { return movie; } else { throw new WebApplicationException(Response.Status.NOT_FOUND); } }
該方法標(biāo)注了 @GET
,表示用來處理向 http://localhost/ms/rest/movie/{id} 發(fā)送的 GET 請(qǐng)求。@Path
再次用來綁定路徑,注意其參數(shù) {id}
,它帶有花括號(hào),對(duì)應(yīng) URI 的最后一段,也正好和方法參數(shù) id
的 @PathParam
的值相對(duì)應(yīng)。這種參數(shù)還有很多高級(jí)用法,以后再介紹。@Produces
注解指定輸出格式為 JSON。JAX-RS 內(nèi)置了很多格式,詳見 MediaType
的文檔。如果找到了相應(yīng) ID 的對(duì)象,則將其返回,JAX-RS 會(huì)自動(dòng)加上響應(yīng)碼 200 OK;否則拋出異常,錯(cuò)誤碼為 404 Not Found。
例如,通過瀏覽器訪問 http://localhost/ms/rest/movie/0,得到的結(jié)果為 {"@id":"0","@title":"Avatar"}。
POST 請(qǐng)求用于創(chuàng)建一個(gè)資源。在本例中用來創(chuàng)建一部電影:
@POST @Consumes(MediaType.APPLICATION_JSON) public Response create(Movie movie) { int id = ai.getAndIncrement(); movieMap.put(id, movie.setId(id)); return Response.created(URI.create(String.valueOf(id))).build(); }
由于沒有 @Path
注解,所以 POST 請(qǐng)求的目標(biāo)就直接是 http://localhost/ms/rest/movie。Consumes
和 @Produces
相反,表示接受的數(shù)據(jù)類型,此處 JAX-RS 會(huì)自動(dòng)把 JSON 數(shù)據(jù)轉(zhuǎn)換為 Movie
對(duì)象。返回的響應(yīng)碼為 201 Created,并且?guī)в兴鶆?chuàng)建資源的 URI。
例如,向 http://localhost/ms/rest/movie 發(fā)送 POST 請(qǐng)求,正文為 {"@title": "007"},則可以從 FireBug 的網(wǎng)絡(luò)監(jiān)控中看到返回的響應(yīng)碼,以及頭部中 Location 的值為 http://localhost:8080/rest/service/movie/1。多次發(fā)送該 POST 請(qǐng)求,將會(huì)創(chuàng)建多個(gè)資源,以保證 POST 不是冪等的。
PUT 請(qǐng)求用于創(chuàng)建或更新一個(gè)資源。與 POST 不同,PUT 請(qǐng)求要指定某個(gè)特定資源的地址。在本例中用來更新一部電影的信息:
@PUT @Path("{id}") @Consumes(MediaType.APPLICATION_JSON) public Response update(@PathParam("id") int id, Movie movie) { movie.setId(id); if (movieMap.replace(id, movie) != null) { return Response.ok().build(); } else { throw new WebApplicationException(Response.Status.NOT_FOUND); } }
更新成功就返回 200 OK,否則返回 404 Not Found。這兒先把 movie
對(duì)象的 ID 強(qiáng)制改為 URI 所指定的,以免出現(xiàn)不一致。也可以根據(jù)需求,將不一致作為異常處理,給客戶端返回一個(gè)錯(cuò)誤碼。
順便啰嗦一句,反正代碼在自己手中,樓主也可以把 PUT 搞成非冪等的,例如將 PUT 當(dāng)成 POST 來處理,就像以前把 GET 和 POST 一視同仁那樣。不過咱既然在搞 JAX-RS,就還是要沾染一點(diǎn) REST 風(fēng)格,嚴(yán)格遵守 HTTP 才是。
DELETE 請(qǐng)求用于刪除一個(gè)資源。在本例中用來刪除一部電影:
@DELETE @Path("{id}") public Response delete(@PathParam("id") int id) { if (movieMap.remove(id) != null) { return Response.ok().build(); } else { throw new WebApplicationException(Response.Status.NOT_FOUND); } }
沒什么特別的,該說的前面都說了。
HEAD 和 OPTIONS 請(qǐng)求就忽略吧,用得不太多,也同樣挺簡(jiǎn)單的。
JAX-RS 服務(wù)的部署和部署常規(guī) Web 程序一樣,打包成 war 文件就可以了。最后贊一下 NetBeans 可以為 REST 風(fēng)格的服務(wù)自動(dòng)生成測(cè)試頁面,很好用,雖然在 Firefox 下頁面顯示不正常(對(duì)此我已經(jīng)提了一個(gè) bug),但 IE 是可以的。
JAX-RS(JSR 311 - Java™ API for RESTful Web Services,用于 REST 風(fēng)格的 Web 服務(wù)的 Java™ API)是 Java EE 6 規(guī)范的一部分,其目標(biāo)在于簡(jiǎn)化和標(biāo)準(zhǔn)化用 Java 開發(fā) REST 風(fēng)格的 Web 服務(wù)。雖然 Java EE 6 剛出爐的時(shí)候,樓主也從頭到尾看過這份規(guī)范,但苦于沒有實(shí)際的項(xiàng)目練手,看過又忘了,現(xiàn)在最多算達(dá)到大成傻逼的境界。這次邊看邊寫,期望完成后至少能破入小成牛逼。先從 REST 本身開始。
REST(REpresentational State Transfer,代表性狀態(tài)傳輸)自稱是一種風(fēng)格而非標(biāo)準(zhǔn),這在樓主看來有炒作的嫌疑。如果僅僅是一種風(fēng)格,那么不同的框架如何兼容?所以才有 JAX-RS 的誕生。REST 最大的貢獻(xiàn)是帶來了 HTTP 協(xié)議的復(fù)興。為什么叫復(fù)興呢?本來 HTTP 的功能挺豐富的,可惜長(zhǎng)期以來只被用作傳輸數(shù)據(jù),大好青年被埋沒了。樓主記得剛開始學(xué) Servlet 的時(shí)候,一向是把 doGet
和 doPost
兩個(gè)方法一視同仁的,因?yàn)闀线@么教,很多 Web 框架也這么搞,以至于弄了很久才搞清楚 GET
和 POST
是兩種不同的請(qǐng)求。現(xiàn)在 REST 拍磚說道,HTTP 早就定義好了一堆操作,以前一直被混淆使用,現(xiàn)在應(yīng)該重新賦予它們本來的意義了,而且如果充分發(fā)揮 HTTP 的功能,完全能夠勝任分布式應(yīng)用的開發(fā)(傳說中的 SOA)。
SOA 的理念在于將系統(tǒng)設(shè)計(jì)為一系列可重用的、解耦的、分布式的服務(wù)。這也不是新鮮玩意兒了,早期有 CORBA,稍晚有 SOAP 等等。REST 作為后起之秀,能夠快速崛起,也必有其非同尋常的特色。下面一一列舉。
系統(tǒng)中的每個(gè)資源都可以通過唯一標(biāo)識(shí)符來訪問。小插一句,“標(biāo)識(shí)”的正確讀音是 biāozhì。REST 使用 URI(統(tǒng)一資源標(biāo)識(shí)符)管理資源的地址。URI 的概念不解釋。一個(gè) URI 可以指向一個(gè)或者多個(gè)資源。
實(shí)際上強(qiáng)調(diào)了 HTTP 操作的原意。REST 主要使用了 GET、PUT、DELETE、POST、HEAD 和 OPTIONS 這 6 種操作。此處有兩個(gè)曾經(jīng)被忽略的 HTTP 概念:冪等(idempotent)和安全(safe)。冪等應(yīng)該是 HTTP 從數(shù)學(xué)借來的一個(gè)術(shù)語(原始的數(shù)學(xué)意義樓主也不懂),意味著若干次請(qǐng)求的副作用與單次請(qǐng)求相同,或者根本沒有副作用。GET、PUT、DELETE、HEAD 和 OPTIONS 都是冪等的:GET、HEAD 和 OPTIONS 都是讀操作,容易理解;PUT 用于創(chuàng)建或更新已知 URI 的資源,多次創(chuàng)建或更新同一個(gè)資源顯然和一次的效果相同;DELETE 刪除資源,亦然。安全表示操作不會(huì)影響服務(wù)器的狀態(tài)。GET、HEAD 和 OPTIONS 是安全的。POST 既不冪等又不安全,因?yàn)楹?PUT 不同,POST 創(chuàng)建資源的時(shí)候并不知道資源的 URI,所以多個(gè) POST 請(qǐng)求將會(huì)創(chuàng)建多個(gè)資源。
表象這個(gè)詞有點(diǎn)拗口,傳聞在一個(gè) REST 風(fēng)格的系統(tǒng)中,服務(wù)端和客戶端之間傳輸?shù)倪诉司褪潜硐蟆硐罂梢允羌兾谋尽ML、JSON……或者自編的山寨格式。唉,不就是數(shù)據(jù)么?只不過可以用任意的格式來傳輸,因?yàn)?HTTP 正文里面啥都能放。Content-Type
頭用來聲明格式,一般是 MIME(多用途因特網(wǎng)郵件擴(kuò)展),像 text/plain
、text/html
、application/pdf
這些。MIME 可以帶屬性,例如 text/html; charset=UTF-8
。
REST 服務(wù)器只管理資源,而不會(huì)像 Web 服務(wù)器一樣記錄客戶的會(huì)話狀態(tài),這些應(yīng)該由客戶端來管理,如此就能增強(qiáng) REST 服務(wù)器的伸縮性。此處的客戶端可以是客戶端程序、瀏覽器,甚至一個(gè) Web 應(yīng)用。總之,REST 只負(fù)責(zé)庫管啦!
猛詞砸來了!HATEOAS = Hypermedia As The Engine Of Application State,超媒體作為應(yīng)用狀態(tài)的引擎,怎么看起來比 SaaS(Software as a Service,軟件作為服務(wù))還要嚇人呢?超文本不過是一只紙老虎,超媒體也瞞不過樓主的天眼:超媒體就是是由文字、圖像、圖形、視頻、音頻……鏈成一體的大雜燴!很簡(jiǎn)單的一個(gè)例子,有些坑爹的電影網(wǎng)站經(jīng)常發(fā)布一些內(nèi)嵌了廣告的電影,播放的時(shí)候會(huì)彈出廣告窗口,里面很多鏈接,你去點(diǎn)兩下就中招了:這個(gè)電影文件就算是超媒體。
其實(shí)這個(gè)詞最關(guān)鍵的地方是“狀態(tài)引擎”。例如樓主在去網(wǎng)購,先選了幾個(gè)東西,接下來可以干啥呢?可以繼續(xù)選、可以把購物車清空、可以結(jié)賬……樓主可以從現(xiàn)狀“轉(zhuǎn)換”到其他某些狀態(tài),而控制狀態(tài)轉(zhuǎn)換的那個(gè)咚咚就被冠名為狀態(tài)引擎。多么聰明的詞匯啊!樓主發(fā)現(xiàn)凡是高手都是造詞磚家呀!用超媒體來控制狀態(tài)轉(zhuǎn)換,就是 HATEOAS:你是要繼續(xù)看電影還是看廣告?看哪個(gè)廣告?自己慢慢考慮……
REST 相比 CORBA、SOAP、WS-* 之流確實(shí)獨(dú)樹一幟,但也難逃玩弄概念的嫌疑。記得大學(xué)里講數(shù)據(jù)庫的老師說過:“你們現(xiàn)在學(xué)了這么多理論,其實(shí)以后在工作中未必管用。在大街上隨便找一個(gè)軟件培訓(xùn)學(xué)校出來的小伙子,他哪兒懂什么第二第三范式啊?但卻能把數(shù)據(jù)庫玩兒得飛轉(zhuǎn)!”