JAX-RS 使用注解進(jìn)行配置,所以用它開發(fā) REST 風(fēng)格的服務(wù)非常簡(jiǎn)單。樓主在本文用一個(gè)小例子來(lái)說(shuō)明 JAX-RS 的基本用法。
假設(shè)樓主要開發(fā)一個(gè)小電影服務(wù),客戶端可以通過(guò)請(qǐng)求 URI 對(duì)電影進(jìn)行 CRUD 操作。為簡(jiǎn)明起見,這兒不使用數(shù)據(jù)庫(kù),只在內(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
等注解。接下來(lái)看看如何編寫 JAX-RS 服務(wù)。
一個(gè) JAX-RS 服務(wù)就是一個(gè)使用了 JAX-RS 注解來(lái)將 HTTP 請(qǐng)求綁定到方法的 Java 類,一共支持兩種類型:?jiǎn)握?qǐng)求對(duì)象或單例對(duì)象。單請(qǐng)求對(duì)象意味著每來(lái)一個(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ù)可通過(guò)繼承 javax.ws.rs.core.Application
來(lái)定義,其中的 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
注解來(lái)指定單例對(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)行,此處略過(guò)。這項(xiàng)配置必不可少,否則無(wú)法部署服務(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ù)庫(kù)”,所以用單例對(duì)象即可,此處使用 CDI 的 @javax.inject.Singleton
來(lái)聲明單例。@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)建者模式的味道。
接下來(lái)看看如何處理 HTTP 請(qǐng)求。
GET
GET 請(qǐng)求用于獲取一個(gè)或多個(gè)資源。在本例中用來(lái)獲取一部電影的信息:
@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
,表示用來(lái)處理向 http://localhost/ms/rest/movie/{id} 發(fā)送的 GET 請(qǐng)求。@Path
再次用來(lái)綁定路徑,注意其參數(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。
例如,通過(guò)瀏覽器訪問(wèn) http://localhost/ms/rest/movie/0,得到的結(jié)果為 {"@id":"0","@title":"Avatar"}。
POST
POST 請(qǐng)求用于創(chuàng)建一個(gè)資源。在本例中用來(lái)創(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();
}
由于沒(méi)有 @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
PUT 請(qǐng)求用于創(chuàng)建或更新一個(gè)資源。與 POST 不同,PUT 請(qǐng)求要指定某個(gè)特定資源的地址。在本例中用來(lái)更新一部電影的信息:
@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 來(lái)處理,就像以前把 GET 和 POST 一視同仁那樣。不過(guò)咱既然在搞 JAX-RS,就還是要沾染一點(diǎn) REST 風(fēng)格,嚴(yán)格遵守 HTTP 才是。
DELETE
DELETE 請(qǐng)求用于刪除一個(gè)資源。在本例中用來(lái)刪除一部電影:
@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);
}
}
沒(méi)什么特別的,該說(shuō)的前面都說(shuō)了。
HEAD 和 OPTIONS 請(qǐng)求就忽略吧,用得不太多,也同樣挺簡(jiǎn)單的。
JAX-RS 服務(wù)的部署和部署常規(guī) Web 程序一樣,打包成 war 文件就可以了。最后贊一下 NetBeans 可以為 REST 風(fēng)格的服務(wù)自動(dòng)生成測(cè)試頁(yè)面,很好用,雖然在 Firefox 下頁(yè)面顯示不正常(對(duì)此我已經(jīng)提了一個(gè) bug),但 IE 是可以的。