JAX-RS(Java API for RESTful Web Service,JSR-311)是Java提供用于開發RESTful Web服務基于注解(annotation)的API,
在Java EE 6中釋出,旨在定義一個統一的規範,使得Java程式員可以使用一套固定的接口來開發REST應用,避免了依賴第三方框
架,同時JAX-RS使用POJO程式設計模型和基于注解的配置并內建JAXB,進而有效縮短了REST應用的開發周期,JSR-311開始于2007
年2月,至今釋出了兩個最終版本1.0,1.1,值得注意的是JAX-RS2.0(JSR-339,始于2011年1月)正在進行中;
(順便說下,Java之是以稱為開放技術就是因為JSR(Java Specification Requests),任何組織和個人都能向JCP(Java
Community Process)提起JSR,經專家組評審通過後可以在後續Java版本中作為新功能釋出,Java的發展就是通過JCP來推動)。
JAX-RS API概述
JAX-RS定義的包結構如下,包含近五十多個接口,注解和抽象類:
javax.ws.rs:包含用于建立RESTful服務資源的高層次(High-level)接口和注解;
javax.ws.rs.core:包含用于建立RESTful服務資源的低層次(Low-level)接口和注解;
javax.ws.rs.ext:包含用于擴充JAX-RS API支援類型的APIs;
注意:JAX-RS規範隻是定義API,真正開發RESTful Web服務需要引入具體實作,具體實作由第三方提供,如Sun的參考實作Jersey,Apache CXF,Jboss RESTEasy。
JAX-RS API的目标
- 基于POJO:該API提供一組注解,類,接口用于将POJOs暴露為網絡資源(Web Resource),并定義對象的生命周期和有效範圍;
- 以HTTP為中心:該規範将預設HTTP作為底層網絡協定并提供一個清晰的HTTP和URI元素到API類和注解之間的映射,該API提供對常見的HTTP使用模式和包括WebDAV,Atom釋出協定在内的各種HTTP應用進行支援;
- 格式無關性:該API将能處理多種不同内容格式的HTTP實體,并提供統一的擴充機制允許應用新增支援其他内容格式(通過實體Provider);
- 容器無關性:使用該API的應用能被部署到多個Web容器,該規範将定義如何将應用部署在Servlet容器和作為一個JAX-WS提供者;
- 融入Java EE:該規範将定義在Java EE容器中網絡資源(Web Resource)類的環境并将指導如何使用Java EE提供的功能群組件。
下面看到一篇比較好的介紹JAX-RS和REST的文章,轉載過來一起學習。
一、簡介
Java Web有很多成熟的架構,主要可以分為兩類Web Application和Web Services。用于Web Application的架構包括官方的Servlet/JSP, JSTL/JSF以及第三方Struts/spring MVC(action-based)。Web Services的項目又可以分為基于XML的(SOAP/WSDL)的和基于JSON的,Java Communitiy為這兩種方式都定義了标準,Java EE5引入了JAX-WS(Java API for XML Web Services)-JSR224,Java EE6引入了JAX-RS(Java API for RESTful Web Services)-JSR331。RESTful Service由于輕量,好測試,有彈性等特點,越來越受歡迎。Jersey,RESTEasy都是JAX-RS标準的具體實作。
二、REST
Rest(representational state transfer, 表現層狀态轉化)是一種漸漸變成Web設計主流的設計理念,最早由Roy Thomas Fielding(HTTP1.0/1.1協定主要設計者之一,Apache作者之一,Apache基金會第一任主席)在2000年的博士論文中提出。
- 資源(Resource):網絡上一個實體(具體資訊),每個資源都用一個URI來辨別和定位。所有的資源都位于伺服器中。
- 表現層(Representation):資源的表現形式。例如文本資訊可以用Txt展現,也可以用HTML,XML,JSON格式表現,甚至是二進制格式。URI隻代表資源實體,它的表現形式在Http請求頭中用Accept和Content-Type字段指定,這兩個字段才是對表現層的描述。用戶端見到的所有東西都隻是伺服器上資源的表現層,用戶端和伺服器之間傳遞的也都是表現層(資源請求攜帶的參數,傳回的JSON,TXT,JPG等MIME-TYPE)。
- 狀态轉換(State Transfer):用戶端所有操作本質上就是用某種方法讓伺服器中的資源狀态發生變化。用戶端隻能見到資源的表現層,是以伺服器上資源狀态的轉換必然建立在表現層上。用戶端讓伺服器資源發生狀态變化的唯一方法就是使用HTTP請求,通過HTTP請求的不同方法(Method)實作對資源的不同的狀态更改操作(如增删改查Create,Read,Update,Delete)。HTTP協定中設計的請求方法包括GET(擷取),POST(新增),PUT(更新),DELETE(删除),HEAD,STATUS,OPTIONS等,不同方法代表了不同的操作,但是HTML隻實作了GET和POST。
示例,例如有一個圖書管理的Restful服務,該服務将會呈現為下面的形式(先不用考慮服務具體如何實作的):
資源:
系統中所有書籍的集合是一個資源,可以用URL http://www.example.com/books 來表示
系統中有本書id為1000,這本書也是一個資源,可以用URL http://www.example.com/books/1000 來表示
操作:
如果想要檢視書集中包含哪些具體的書,可以使用GET方法請求集合資源:
GET http://www.example.com/books
如果想要檢視id為1000這本書的詳細資訊,可以GET方法請求單本書的資源:
GET http://www.example.com/books/1000
如果想新增一本書,可以使用POST方法請求集合資源(假如成功後自動生成id為1001):
POST http://www.example.com/books { {'name' : ' good book'}, {'price': 100}}
如果想修改一本書,可以使用PUT方法請求書的資源:
PUT http://www.example.com/books/1001 { {'price': 98} }
如果想删除id為1000的書,可以使用DELETE方法請求單本書的資源:
DELETE http://www.example.com/books/1000
特别說明
- URI中不應該包含動詞。資源表示的一種實體,應該都是名詞。隻能用HTTP請求方法表示資源操作動作。
例如/posts/show/1 應該改為/posts/1 用GET方法表明是show操作。
- 有些難以用請求方法直接表達的動作可以換成名詞,作為服務性的資源。
例如 transfer動作 可以修改為POST /transaction from=1&to=2&amout=100.00
- URI中不應該包含版本号。不同的版本其實是同一種資源的不同表現層,所有應該使用同一個URI。版本号在HTTP請求頭的Accept字段中區分(參考 http://www.informit.com/articles/article.aspx?p=1566460)。
例如
http://www.example.com/app/1.0/foo
http://www.example.com/app/2.0/foo
在請求頭中區分:
Accept: vnd.example-com.foo+json; version=1.0
Accept: vnd.example-com.foo+json; version=2.0
三、JAX-RS
JAX-RS和所有JAVA EE的技術一樣,隻提供了技術标準,允許各個廠家有自己的實作版本,實作版本有:RESTEasy(JBoss), Jersey(Sun提供的參考實作), Apache CXF, Restlet(最早的REST架構,先于JAX-RS出現), Apache Wink。JAX-RS基于JavaEE的Servlet。标準中定義的注解大大簡化資源位置和參數的描述,僅僅使用注解就可以将一個POJO java類封裝成一個Web資源。JAX-RS也有類似于Spring依賴注入的方式,減少類之間的耦合度。
JAX-RS标準的一個簡單RESTful Web Service示例,例如有一個greeter的資源,URI為http://localhost:8080/greeter/
@Path("/greeter")
public class GreeterResource {
@GET
@Path("/{name}")
public String sayHello(@PathParam("name") String name) {
return "Hello, " + name;
}
@DELETE
@Path("/{name}")
public String sayBye(@PathParam("name") String name) {
return "Bye, " + name;
}
}
使用GET方法請求該資源 (http://localhost:8080/greeter/tom)
将得到輸出: Hello, tom
使用DELETE方法請求該資源 (http://localhost:8080/greeter/lily)
将得到輸出: Bye, lily
如果把上面的資源類定義為接口, 将REST服務的定義和實作分離是一種更好的實作方式。代碼更簡潔清晰,後期修改也更友善。
四、JAX-RS注解
1.資源類或方法的相對路徑注解
@Path
若希望一個Java類能夠處理REST請求,則這個類必須至少添加一個@Path("/")的annotation;對于方法,這個annotation是可選的,如果不添加,則繼承類的定義。
Path裡的值可以是複雜表達式,例如@Path("{id}"),其中的{xxx}表示一個模闆參數,模闆參數是定義在@Path裡的通配符,它以 { 開始,中間是一堆字母和數字的混合串(不能包含 / 字元),以} 結尾。又如: @Path("{firstName}-{lastName}")
Path也支援正規表達式,例如:@Path("{id: \\d+}")
優先級檢查規則(如果這樣的規則還不能解決問題,那就是設計的過于複雜了):
-
- 首先檢查比對的字元個數,越多越優先;
- 其次檢查内嵌的模闆表達式個數,越多越優先;
- 最後檢查非預設模闆表達式個數(預設模闆即未定義正規表達式的模闆)
例如
-
- /customers/{id}/{name}/address
- /customers/{id : .+}/address
- /customers/{id}/address
- /customers/{id : .+}
Path的字元(如果Path中的表達式包含需要轉義的字元,JAX-RS會自動進行轉義;否則會認為以及進行過URL Encoding)
-
- 允許a-z, A-Z, 0-9
- 允許_-!.~'()*
- 保留(需轉義),;:$$+=?/[]@
- 其字元都需要用%HH轉義
子資源定位符(Subresource Locators),一個指定了@Path注解但未指定HttpMethod注解的方法,該方法可以傳回另一個資源類對象,讓這個對象接着分發和處理請求子資源的請求。子資源類并不需要作為服務對外暴露,是以類上可以不用加@Path注解。
@Path("/customers")
public class CustomerResource {
......
@Path("{database}-db")
public CustomerResource getDatabase(@PathParam("database") String db) {
// find the instance based on the db parameter
CustomerResource resource = locateCustomerResource(db);
return resource;
}
protected CustomerResource locateCustomerResource(String db) {
...
}
......
}
public class CustomerResource { @GET
@Path("{id}")
@Produces("application/xml")
public StreamingOutput getCustomer(@PathParam("id") int id) {
...
}
@PUT
@Path("{id}")
@Consumes("application/xml")
public void updateCustomer(@PathParam("id") int id, InputStream is) {
...
}
}
完全動态分發。上面的例子中指定了@Path注解但未指定HttpMethod注解的方法,該方法可以傳回任何類對象。JAX-RS會檢查這個對象并自動決定如何分發和處理請求。
@Path("{database}-db")
public Object getDatabase(@PathParam("database") String db) {
if(db.equals("europe"))
return locateCustomerResource(db);
return "not supported db";
}
2.請求方法注解
@GET, @PUT, @POST, @DELETE, @HEAD, @OPTIONS 方法可以處理的HTTP請求方法類型
一個方法隻有添加了請求方法注解,才能處理請求。 JAR-RS的實作中一般都預定義了HEAD 和OPTIONS方法(例如Jersery中HEAD方法會調用GET但不傳回内容體,OPTIONS方法傳回一個WADL格式的或資源或資源方法的描述)
可以自定義請求方法注解,但不要重寫HttpMethod定義的注解(GET,POST,PUT,DELETE,HEAD,OPTIONS)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("LOCK")
public @interface LOCK {
}
3.參數注入注解
每個資源方法最多隻能有一個沒有注解的參數。這個沒有注解的參數為請求體(entity)的内容
(1) @PathParam, @QueryParam, @HeaderParam, @CookieParam, @MatrixParam, @FormParam 參數來自HTTP請求的不同位置
如果是“每個請求一個對象的模式”,當JAX-RS收到一個請求時會查找相應的資源方法,然後把方法需要的參數注入。除了資源方法,這些注解還可以用在變量,set方法或者構造方法上。如果是單例模式,這些注解就不能用在變量或set方法上了,負責可能産生沖突。
- 注入參數時會自動嘗試進行類型轉換。
@Path("{id}") @GET public StreamingOutput getInfo(@PathParam("id") int id){...}
- 可以同時注入多個參數
@Path("{first}-{last}") @GET public StreamingOutput getInfo(@PathParam("first") String firstName, @PathParam("last") String lastName) {...}
- 總是引用最近的Path中的值
@Path("/customers/{id}") class...
@Path("/address/{id}") @GET public String getInfo(@PathParam("id") String addressId) {...}
例如如果請求為GET /custormers/123/address/456, addressID被注入456
- 注入路徑片段(PathSegment),還可以擷取PathSegemt中的MatrixParam,PathSegment定義如下:
package javax.ws.rs.core;
public interface PathSegment {
String getPath(); //具體的URI的path片段值,去除了所有的matrix參數
MultivaluedMap<String, String> getMatrixParameters(); //該path片段擁有的所有的matrix值
}
@Path("/cars/{make}") class...
@Path("/{model}/{year}") @GET @Produces("imge/jpeg") public Jpeg getPic(@PathParam("make") String make, @PathParam("model") PathSegment car, @PathParam("year") String year){
String color=car.getMatrixParameters().getFirst("color");
...}
例如:GET /cars/mercedes/e55;color=black/2006。則 make是mercedes;car是e55;year是2006;color是black。
- 注入多個路徑片段
@Path("/cars/{make}") class...
@Path("/{model : .+}/year/{year}") @GET @Produces("imge/jpeg") public Jpeg GetPic(@PathParam("make") String make, @PathParam("model") List<PathSegment> car, @PathParam("year") String year){...}
例如:GET /cars/mercedes/e55/amg/year/2006。 model片斷比對 /e55/amg, 是以car變量中包含兩個PathSegment對象。
- MatrixParam
Matrix Param是一個嵌入在URI字元串中的name:value對,修飾Path中的一個片段。例如http://example.cars.com/mercedes/e55;color=black/2006。Matrix Parm對@Path表達式是透明的,這個例子中還是使用@Path("/e55/{year}")。但是可以用@MatrixParam 注解将MatrixParam的值注入到方法參數中。如果路徑中包含多個同名的Matrix Param還是需要用PathSegment來擷取。
@GET public String getInfo(@MatrixParam("color") String color){...}
- QueryParam
提取查詢參數,也可以用UriInfo擷取
- FormParam
提取Post請求中的Form參數,其中Content-Type被假設為application/x-www-formurlencoded
- HeaderParam
提取Http Header值,可以用@Content注入HttpHeaders對象提取所有Header值。
- CookieParam
提取Cookie值,不僅可以注入基本類型,還可以注入Cookie對象。也可以用@Context注入HttpHeaders對象擷取所有Cookie資訊。
- BeanParam (JAX-RS 2.0)
将其他的xxxParam封裝到一個Bean當中,并把這Bean作為參數注入。可以重複使用參數定義也可以做一些Bean驗證的操作。
public class MyBean {
@FormParam("myData")
private String data;
@HeaderParam("myHeader")
private String header;
@PathParam("id")
public void setResourceId(String id) {...}
...
}
@POST
@Path("{id}")
public void post(@BeanParam MyBean myBean) {...}
(2) @Context 注入輔助對象或資訊對象
- ServletContext
- ServletConfig
- HttpServletRequest
使用較新的版本的Jersery時,用注入的request對象讀取post請求的參數會傳回null,詳見https://java.net/jira/browse/JERSEY-766。正确的方式是用JAX-RS的參數注解或者使用MultivaluedMap注入參數。
對于multipart/form-data的post請求,可以注入request對象并将其傳遞給fileupload,jspsmartupload等第三方工具類,由這些第三方類解析得到參數和上傳的檔案内容(參考:http://www.cnblogs.com/pixy/p/4868188.html)。
- HttpServeltResponse
- UriInfo對象,可以擷取路徑資訊
@Path("/cars/{make}") class...
@Path("/{model}/{year}") @GET @Produces("imge/jpeg") public Jpeg GetPic(@Context UriInfo info)
{
String make=info.getPathParameters().getFirst("make");
PathSegment model=info.getPathSegments().get(1);
String color=model.getMatrixParameters().getFirst("color");
...
}
例如:GET /cars/mercedes/e55;color=black/2006。
- HttpHeaders對象,可以擷取所有Header資訊(還包括Cookie資訊)
(3)注入時類型轉換
參數注入時預設都是String類型。也可轉換成滿足下面條件之一的Java類型。如果轉換失敗,則認為client請求出錯,傳回404錯誤。
- 基本類型: int, short, float, double, byte, char, boolean
- 帶單個String參數的構造方法。如@HeaderParam("Referer") URL referer
- 擁有一個static的valueOf(String)方法,這個方法傳回該類型的一個執行個體。如enum類型
- java.util.List<T>, java.util.Set<T>或java.util.SortedSet<T>,其中的T滿足2或3,或者為String
(4)@DefaultValue 定義預設值
@DefaultValue可以給某個請求參數定義預設值,當Client的請求中未包含此參數時,則預設值被使用。
@GET public String getCustomers(@DefualtValue("0") @QueryParam("start") int start, @DefaultVaue("10") @QueryParam("size") int size){...}
(5)@Encoded 強制不解碼
該注解标明此參數不需要自動解碼,直接使用編碼後的請求值。
@GET String getInfo(@Encoded @QueryParam("size") int size){...}
(6)其他類型的請求讀取和響應寫出
詳細示例:http://liugang594.iteye.com/blog/1499638
- StreamOutput接口寫出響應。自由靈活,高性能,可異步響應
public StreamingOutput getCustomer(int id) {
final Customer customer = customerDB.get(id);
if (customer == null) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
return new StreamingOutput() {
public void write(OutputStream outputStream) throws IOException,
WebApplicationException {
outputCustomer(outputStream, customer);
}
};
}
- InputStream/Reader讀入請求或寫出響應(寫出響應時需要用@Produces設定Content-Type頭資訊)
- File讀入請求或寫出響應(背景将請求寫入臨時檔案,再把檔案作為參數傳入,寫出響應時需要用@Produces設定Content-Type頭資訊)
- byte[] 讀入請求或寫出響應(寫出響應時需要用@Produces設定Content-Type頭資訊)
- String/Char[] 讀入請求或寫出響應(按照Content-Type中指定的charset處理,寫出響應時需要用@Produces設定Content-Type頭資訊)
- MultvaluedMap<String,String> 讀入請求或寫出響應(包含所有的Form資料,@Consumes格式必須為application/x-www-urlencoded,大多數實作者會自動解碼,如果不想自動解碼可用@Encoded注解)
- Source XML的輸入或輸出,通常用來進行XSLT轉換(@Produces,@Consumes格式為application/xml)
- JAXB對象自動轉換。如果交換類型為application/xml,text/xml,application/*+xml,并且對象類型包含JAXB注解限制的類。内置的JAXB處理器可以自動進行轉換。(更多内容可以檢視 http://liugang594.iteye.com/blog/1499813)
- 手動建立Response響應對象。 Resopnse對象可通過ResponseBuilder建立。還可以使用NewCookie對象,可以Response.status()和ResponseBuilder.status()都可以接受Status枚舉值作為參數。GenericEntity可以為泛型對象轉換為确定的類型并作為Entity賦給Response。
@GET
@Path("/info")
@Produces("text/plain")
public Response getInfo() {
GenericEntity entity = new GenericEntity<List<Customer>>(infolist){};
ResponseBuilder builder = Response.ok(entity);
//ResponseBuilder builder = Response.ok(info);
builder.language("fr").header("Some-Header", "some value");
NewCookie cooike=new NewCookie("key","value");
builder.cookies(cookie);
return builder.build();
}
@Produces 傳回的MIME媒體類型,(可以注解方法或類,方法上的注解或覆寫類的注解),例如application/xml
@Consumes 可接受請求的MIME媒體類型,(可以注解方法或類,方法上的注解或覆寫類的注解)例如application/xml
五、JAX-RS異常處理
HTTP中定義的響應狀态碼
響應碼 | 含義 |
100 | 繼續 |
101 | 分組交換協定 |
200 | OK |
201 | 被建立 |
202 | 被采納 |
203 | 非授權資訊 |
204 | 無内容,傳回值為null或void |
205 | 重置内容 |
206 | 部分内容 |
300 | 多選項 |
301 | 永久地傳遞 |
302 | 找到 |
303 | 參見其他 |
304 | 未改動 |
305 | 使用代理 |
307 | 暫時重定向 |
400 | 錯誤請求 |
401 | 未授權 |
402 | 要求付費 |
403 | 禁止 |
404 | 未找到,網頁/Path未找到 |
405 | 不允許的方法,請求的方法未找到 |
406 | 不被采納,請求的期望傳回交換類型不比對 |
407 | 要求代理授權 |
408 | 請求逾時 |
409 | 沖突 |
410 | 過期的 |
411 | 要求的長度 |
412 | 前提不成立 |
413 | 請求執行個體太大 |
414 | 請求URL太大 |
415 | 不支援的媒體類型 |
416 | 無法滿足的請求範圍 |
417 | 失敗的預期 |
500 | 内部錯誤 |
501 | 未被使用 |
502 | 網關錯誤 |
503 | 不可用的服務 |
504 | 網關逾時 |
505 | HTTP版本未被支援 |
WebApplicationException
WebApplicationException是一個内置的、非檢測異常,支援傳入Response對象或者狀态碼。
當JAX-RS碰到一個WebApplicationException抛出時,它會捕獲這個異常,調用getRespnse()方法擷取Response,發回給用戶端。如果應用以一個狀态碼或者Response初始化了WebApplicationException,則這個狀态碼或者Response将被用來建立真正的Http響應;否則會直接傳回500伺服器内部錯誤給用戶端。
@GET
@Path("{id}")
@Produces("application/xml")
public Customer getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
if (cust == null)
throw new WebApplicationException(Response.Status.NOT_FOUND); #如果沒有找到客戶,則傳回404錯誤(不推薦這麼用)
return cust;
}
ExceptionMapper接口
比對一個抛出的異常到一個Response對象。ExceptionMapper的實作必須加上@provider注解。ExceptionMapper支援異常層級關系,例如A繼承B, 如果找不到A的mapper,則會向上找B的mapper。ExceptionMapper使用JAX-RS的deployment API注冊(用Application)。
@Provider
public class EntityNotFoundMapper
implements ExceptionMapper<EntityNotFoundException> {
public Response toResponse(EntityNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
六、JAX-RS與内容協定
用戶端在請求中告訴伺服器它期望的傳回消息體内容格式,包括使用什麼資料格式,怎麼編碼,使用哪國語言,服務接口的哪個版本等。這種協定被稱為Http Content Negotiation(Http内容協定,簡稱Conneg)。
1.Http的Media Type(用戶端期待的傳回媒體類型)
通過Http請求頭的Accept字段指定,Accept字段中的多個内容也逗号分隔。取值為MIME Type(可以用分号附加屬性),也可以使用通配符。如果指定了多個類型,伺服器傳回任何一種即可,如果伺服器傳回的類型不一緻,則得到406 Not Acceptable響應碼。
GET http://example.com/stuff
Accept: application/xml, application/json, text/*
優先級:
- 隐式規則:越具體的優先級越高。
例如:Accept: text*, application/xml
優先級為: text/html;level=1(包含屬性) > application/xml > text*
- 用戶端使用MIME Type的 q 屬性顯示指定優先順序(q的取值範圍為0.0~1.0,預設為1.0)
例如:text*;q=0.1, audio/mpeg, application/xml;q=0.5
優先級為: audio/mpeg(q=1.0) > txex*
2.Http的語言約定
用戶端可以在請求頭中使用Accept-Language字段指定他們需要接受哪個語種。内容值為ISO-639*中定義的語言映射代碼,兩個字母表示語言,還可以再增加兩個字母更具體的表示哪個國家的哪種語言,例如: en-US表示美國使用的英語。也可以中 q 屬性指定優先級。
GET http://example.com/stuff
Accept-Language: en-us, es, fr;q=1.0
伺服器響應頭中使用Content-Language告訴用戶端傳回内容的語種。
3.Http的壓縮約定
HTTP支援内容壓縮以節省帶寬,最通用的壓縮算法為GZIP。用戶端可以在請求頭的Accept-Encoding字段指定支援的壓縮算法。也支援q參數指定優先級。deflate表示不壓縮。
GET http://example.com/stuff
Accept-Encoding: gzip;q=1.0, compress;q=0.5; deflate;q=0.1
4.JAX-RS的@Produces注解
該注解可以訓示響應的Media Type。JAX-RS也使用這個注解将請求分發到@Produces注解内容與請求中Accept字段最佳比對的方法上。
例如:
@Path("/customers")
public class CustomerResource {
@GET
@Path("{id}")
@Produces("application/xml")
public Customer getCustomerXml(@PathParam("id") int id) {...}
@GET
@Path("{id}")
@Produces("text/plain")
public String getCustomerText(@PathParam("id") int id) {...}
@GET
@Path("{id}")
@Produces("application/json")
public Customer getCustomerJson(@PathParam("id") int id) {...}
}
GET http://example.com/customers/1
Accept: application/json;q=1.0, application/xml;q=0.5
這個例子中:getCustomerJson()方法是最佳比對,将會被調用
JAXB可以實作從Java對象到XML或Json的映射,使用@Produces注解可以實作一個能服務于這兩種格式的方法。
@GET
@Produces({"application/xml", "application/json"})
public Customer getCustomer(@PathParam("id") int id) {...} //Customer包含JAXB注解
5.使用JAX-RS處理更複雜的内容約定
JAX-RS沒有提供對Language,Encoding等相關的注解。我們需要使用其他方法來處理。 這種情況其實很少見,大多數資源方法使用@produces就已經完全足夠了。
(1)使用@Context注入HttpHeaders接口類型參數。調用HttpHeaders對象的方法getAcceptableMediaTypes(),getAcceptableLanguages(),這兩方法分别傳回MediaType和Local元素類型的List,其中的元素已經按優先級排好序。
@GET
public Response get(@Context HttpHeaders headers) {
MediaType type = headers.getAcceptableMediaTypes().get(0);
Locale language = headers.getAcceptableLanguages().get(0);
Object responseObject = ...;
Response.ResponseBuilder builder = Response.ok(responseObject, type);
builder.language(language);
return builder.build();
}
(2)使用Request和Variant類。Variant類是一個封裝了MediaType, Language和Encoding的結構,表示一個JAX-RS資源方法支援的結合。Request接口中的selectVariant方法可以傳人一個Variant的清單,然後檢查請求頭中的Accept,Accept-Laguage,Accept-Encoding字段,傳回傳入的清單中最佳比對的那個Variant對象。如果沒有符合的就傳回null。一般想要靈活處理Content Encoding最好自己處理所有的流,大多數JAX-RS實作都自動支援GZIP。
@GET
Response getSomething(@Context Request request) {
List<Variant> variants = new ArrayList();
variants.add(new Variant(new MediaType("application/xml"),"en", "deflate"));
variants.add(new Variant(new MediaType("application/xml"),"es", "deflate"));
variants.add(new Variant(new MediaType("application/json"),"en", "deflate"));
variants.add(new Variant(new MediaType("application/json"),"es", "deflate"));
variants.add(new Variant(new MediaType("application/xml"),"en", "gzip"));
variants.add(new Variant(new MediaType("application/xml"),"es", "gzip"));
variants.add(new Variant(new MediaType("application/json"),"en", "gzip"));
variants.add(new Variant(new MediaType("application/json"),"es", "gzip"));
// Pick the variant
Variant v = request.selectVariant(variants);
Object entity = ...; // get the object you want to return
ResponseBuilder builder = Response.ok(entity);
builder.type(v.getMediaType()).language(v.getLanguage()).header("Content-Encoding", v.getEncoding());
return builder.build();
}
這麼一個個列出組合太麻煩,可以使用VariantBuilder類簡化,自動生成是以可能的組合。
@GET
Response getSomething(@Context Request request) {
Variant.VariantBuilder vb = Variant.VariantBuilder.newInstance();
vb.mediaTypes(new MediaType("application/xml"), new MediaType("application/json"))
.languages(new Locale("en"), new Locale("es"))
.encodings("deflate", "gzip");
List<Variant> variants = vb.build();
Variant v = request.selectVariant(variants);
Object entity = ...; // get the object you want to return
ResponseBuilder builder = Response.ok(entity);
builder.type(v.getMediaType()).language(v.getLanguage()).header("Content-Encoding", v.getEncoding());
return builder.build();
}
VariantBuilder類也支援多個不同的組合集合。調用add()方法可以結束前一個組合集合,開始另一個組合集合。
Variant.VariantBuilder vb = Variant.VariantBuilder.newInstance();
vb.mediaTypes(new MediaType("application/xml")).languages(new Locale("en"), new Locale("es")).encodings("deflate", "gzip")
.add()
.mediaTypes(new MediaType("text/plain")).languages(new Locale("en"), new Locale("es"), new Locale("fr")).encodings("compress");
6.在URI中包含約定
Conneg是很強大的Http特性,但還是有些用戶端不支援,請求頭的Accept資訊可能被用戶端寫死了,無法指定(如Firefox)。一種解決方法就是在URI中嵌入約定資訊,(如/customers/en-us/xml/3323或者/customers/3323.xml.en-us)然後在資源方法中通過注入參數擷取到這些資訊。
@Path("/customers/{id}.{type}.{language}")
@GET public Customer getCustomer(@PathParam("id") int id,
@PathParam("type") String type,
@PathParam("language") String language) {...}
很多實作中,會類似于這種方式将URI字尾映射為MediaType和Language,然後替換任何傳入的Accept或Accept-Langage資訊并且删除URI中的這個字尾。
例如
@Path("/customers")
public class CustomerResource {
@GET
@Produces("application/xml")
public Customer getXml() {...}
@GET
@Produces("application/json")
public Customer getJson() {...}
}
請求GET /customers.json, JAX-RS會提取.json字尾,并将字尾從路徑中删除。.json字尾映射成application/json替換請求中的accept字段。最後比對到getJson()資源方法。
MIME type和Java type對應關系
- All media types (*/*)
- byte[]
- java.lang.String
- java.io.Reader (inbound only)
- java.io.File
- javax.activation.DataSource
- javax.ws.rs.core.StreamingOutput (outbound only)
- XML media types (text/xml, application/xml and application/…+xml)
- javax.xml.transform.Source
- javax.xml.bind.JAXBElement
- Application supplied JAXB classes (types annotated with @XmlRootElement [email protected])
- Form content (application/x-www-form-urlencoded)
- MultivaluedMap<String,String>
- Plain text (text/plain)
- java.lang.Boolean
- java.lang.Character
- java.lang.Number
六、相容和演進
當服務子產品不斷地進化時(如添加新的特性,擴充資料,資料格式變化等),還需要保證以前的使用者能在舊版本上運作。
- 添加新的服務子產品并使用新URI自然能解決這個問題(不推薦)。
- URI也可以保持不變,而通過請求不同的MediaType(版本屬性)來通路不同的版本。
一個常用的方案是定義一個新的MediaType,并且使用版本号表示資料的變化。新MediaType的命名指導準則是:vnd(供應商).Company.formatName+MediaType(資料格式基于XML),如vnd.sun.customers+xml
請求中類型名字不變,通過屬性指定版本号: application/vnd.rht.customers+xml;version=1.0
- 可擴充、向前相容的資料結構設計(最佳方案)
結合可擴充向前相容的設計和MediaType版本選擇,才是一個資料格式可更新的系統。版本依賴的客戶可以使用Media Type版本去請求指定的版本資料;未依賴于版本的客戶可以請求和發送他們了解的版本。
七、緩存與并發
HTTP緩存
(1)Expires
HTTP1.0中定義了Expires指定cache在浏覽器中的生命時長。
例如 Expires: Tue, 15 May 2011 16:00 GMT
JAX-RS中使用Response對象設定有效期
@Path("{id}")
@GET
@Produces("application/xml")
public Response getCustomer(@PathParam("id") int id) {
...
ResponseBuilder builder = Response.ok(cust, "application/xml");
Date date = Calendar.getInstance().set(2010, 5, 15, 16, 0);
builder.expires(date);
return builder.build();
}
(2)Cache-Control
HTTP1.1中重新設計了緩存文法。使用Cache-Control字段控制緩存,其中的值一逗号分隔。
- private 指定當且僅當Client才能緩存這個資料
- public 請求/響應鍊中任何環節都可以緩存資料
- no-cache 資料不應該緩存,或除非資料被重新驗證過,否則不能用于再次請求
- no-store 緩存資料通常被存放在硬碟中,該指令表示不要将緩存存在本地
- no-transform 有時緩存被自動轉換以節省記憶體或帶寬,例如壓縮圖像,該指令表示不允許進行資料轉換。
- max-age 指定緩存有效期,如果max-age和Expires同時指定,則max-age有效
- s-maxage 緩存在一個共享的中間節點的最大生命期。
例如: Cache-Control: private, no-store, max-age=300
JAX-RS提供了CacheControl類表示Cache-Control資訊。
@Path("{id}")
@GET
@Produces("application/xml")
public Response getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
CacheControl cc = new CacheControl();
cc.setMaxAge(300);
cc.setPrivate(true);
cc.setNoStore(true);
ResponseBuilder builder = Response.ok(cust, "application/xml");
builder.cacheControl(cc);
return builder.build();
}
重驗證機制(revalidation)
cache變得陳舊時,緩存端能否詢問服務端緩存的資料是否仍然有效。
(3)Last-Modified 和 If-Modified-Since
伺服器在最初的響應中發回一個Last-Modified頭資訊。
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: max-age=1000
Last-Modified: Tue, 15 May 2009 09:56 EST
<body>
用戶端如果支援重驗證,就會存儲這個時間戳到緩存資料。1000秒以後,用戶端可以選擇重新驗證緩存。它會發送一個條件GET請求,将Last-Modified作為If-Modified-Since頭字段内容發給伺服器。
GET /customers/123 HTTP/1.1
If-Modified-Since: Tue, 15 May 2009 09:56 EST
伺服器判斷資料是否變化,如果有變化則傳回200-OK和新的響應體。如果沒有則傳回304-Not Modified和一個空的響應體。這兩種情況都會發送新的Cache-Control和Last-Modified資訊。
(4)ETag 和 If-None-Match
ETag是表示資料版本的、假設唯一的某個辨別。它的值是任一一個用引号括起來的字元串,通常是MD5哈希值。
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: max-age=1000
ETag: "3141271342554322343200"
<body>
類似于Last-Modified頭,如果用戶端緩存了響應體,則也應該緩存該ETag值。1000秒以後,用戶端需要執行一個重驗證請求,其中包含一個If-None-Match的請求頭資訊,其值為緩存的Etag值。
GET /customers/123 HTTP/1.1
If-None-Match: "3141271342554322343200"
伺服器接收到這個Get請求時,會試圖比較目前resource的ETag值和傳入的If-None-Match值,如果不比對則傳回200-OK和新的響應體,否則傳回304-Not Modified和空的響應體。
ETag有兩種類型:
- String ETag: 資源的任何變化都會引起ETag變化
- weak Etag: 隻有資源的顯著變化才會引起ETag變化, 例如:ETag: W/"3141271342554322343200"
JAX-RS中EntityTag類代表ETag。Request對象中提供了ResponseBuilder evaluatePreconditions(xxx)方法處理重驗證。
@Path("{id}")
@GET
@Produces("application/xml")
public Response getCustomer(@PathParam("id") int id,
@Context Request request) {
Customer cust = findCustomer(id);
EntityTag tag = new EntityTag(Integer.toString(cust.hashCode()));
CacheControl cc = new CacheControl();
cc.setMaxAge(1000);
ResponseBuilder builder = request.evaluatePreconditions(tag);
if (builder != null) {
builder.cacheControl(cc);
return builder.build();
}
// Preconditions not met!
builder = Response.ok(cust, "application/xml");
builder.cacheControl(cc);
builder.tag(tag);
return builder.build();
}
}
并發(Concurrency)/條件更新
有條件的更新資料。更新資料時驗證條件,滿足條件才會更新。
首先取得資料:
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: max-age=1000
ETag: "3141271342554322343200"
Last-Modified: Tue, 15 May 2009 09:56 EST
<body>
帶條件的更新,PUT或POST請求中包含ETag或Last-Modified頭資訊,指定了可以更新的條件。這兩個資訊值都來自緩存的Etag和Last-Modified值。
PUT /customers/123 HTTP/1.1
If-Match: "3141271342554322343200"
If-Unmodified-Since: Tue, 15 May 2009 09:56 EST
Content-Type: application/xml
<body>
可以發送If-Match或If-Unmodified-Since中的任何一個。當Server收到這個請求時,就會去檢查目前的ETag是否比對If-Match或目前的時間戳是否比對If-Unmodified-Since頭。如果這些條件都不滿足,則傳回412, 'Precondition Failed'響應,用于告訴用戶端目前資料已經被修改過,請重試;如果條件滿足,則執行更新,并傳回成功的結果。
JAX-RS中也使用Request對象的evaluatePreconditons()方法處理條件更新。
@Path("{id}")
@PUT
@Consumes("application/xml")
public Response updateCustomer(@PathParam("id") int id,
@Context Request request) {
Customer cust = findCustomer(id);
EntityTag tag = new EntityTag(Integer.toString(cust.hashCode()));
Date timestamp = ...; // get the timestampe
ResponseBuilder builder = request.evaluatePreconditions(timestamp, tag);
if (builder != null) {
// Preconditions not met!
return builder.build();
}
//... perform the update ...
builder = Response.noContent();
return builder.build();
}
八、其他
HATEOAS
參考文章:
http://www.codedata.com.tw/java/java-restful-1-jersey-and-jax-rs/
http://www.ruanyifeng.com/blog/2011/09/restful
http://liugang594.iteye.com/category/218423
--------------
未完。。。。
http://blog.csdn.NET/u011970711/article/category/1923271