一、簡介
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/*, text/html;level=1, */*, application/xml
優先級為: text/html;level=1(包含屬性) > application/xml > text/* > */*
- 客戶端使用MIME Type的 q 屬性顯示指定優先順序(q的取值范圍為0.0~1.0,默認為1.0)
例如:text/*;q=0.9, */*;q=0.1, audio/mpeg, application/xml;q=0.5
優先級為: audio/mpeg(q=1.0) > txex/*(q=0.9) > application/xml (q=0.5) > */*
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 or@XmlType)
- 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