JAX-RS入門 九: 內容約定(2)

JAX-RS與Conneg

 

JAX-RS有一些用來幫助用戶管理他的conneg的工具:

  • 基於Accept頭的方法派發
  • 允許直接查看內容信息
  • 用於處理多約束條件的APIs

1. 方法派發

 

前幾節中,我們看到怎麼使用@Produces註釋來指示響應的Media Type。 JAX-RS也使用這個註釋來分發請求到恰當的方法上,通過匹配最佳的請求的Accept頭信息的Media Type 列表到由@Produces標註的元數據。例如:

Java代碼  收藏代碼
  1. @Path("/customers")  
  2. public class CustomerResource {  
  3.   
  4.     @GET  
  5.     @Path("{id}")  
  6.     @Produces("application/xml")  
  7.     public Customer getCustomerXml(@PathParam("id"int id) {...}  
  8.   
  9.     @GET  
  10.     @Path("{id}")  
  11.     @Produces("text/plain")  
  12.     public String getCustomerText(@PathParam("id"int id) {...}  
  13.   
  14.     @GET  
  15.     @Path("{id}")  
  16.     @Produces("application/json")  
  17.     public Customer getCustomerJson(@PathParam("id"int id) {...}  
  18.   
  19. }  

這裏三個方法對應的服務路徑一樣,但是@Produces的元數據不同。JAX-RS會基於請求信息的Accept頭信息來選擇恰當的處理方法,例如:

Java代碼  收藏代碼
  1. GET http://example.com/customers/1  
  2. Accept: application/json;q=1.0, application/xml;q=0.5  

根據頭一節的介紹,這裏,Accept的數據類型的優先級爲:

  1. application/json
  2. application/xml  

因此對這個請求,getCustomerJson()方法將被調用。

 

2. JAXB與Conneg

 

之前的章節裏介紹了怎麼通過使用JAXB來實現從java對象到xml或者json的映射。而通過在JAX-RS中使用conneg,我們也可以實現一個可以服務於這兩種格式的方法,例如:

Java代碼  收藏代碼
  1. @Path("/service")  
  2. public class MyService {  
  3.   
  4.     @GET  
  5.     @Produces({"application/xml""application/json"})  
  6.     public Customer getCustomer(@PathParam("id"int id) {...}  
  7.   
  8. }  

 

3. 複雜的內容協議

 

有時候,簡單的Accept頭與@Produces之間的映射可能不足以解決問題,服務於同一路徑的不同的JAX-RS方法可能需要處理不同的Media Type、Language、Encoding等等。 不幸的是JAX-RS並沒有提供類似於@ProduceLanguages或者@ProduceEncodings註釋。相反,我們必須實現自己的頭信息查找方法,或者是使用JAX-RS API管理複雜的Conneg。接下來分別介紹這兩種方式。

 

        > 查看Accept頭

在之前的章節裏介紹過javax.ws.rs.core.HttpHeaders接口。這個接口包含了預處理的與Http請求相關的Conneg信息:

Java代碼  收藏代碼
  1. public interface HttpHeaders {  
  2.   
  3.     public List<MediaType> getAcceptableMediaTypes();  
  4.     public List<Locale> getAcceptableLanguages();  
  5.     ...  
  6.   
  7. }  

 

getAcceptableMediaTypes()得到包含定義在HTTP請求的Accept頭信息的Media Type列表,其中的項被解析成一個個javax.ws.rs.core.MediaType對象,並且這個列表已經是基於其中的"q"值(顯式或隱式的)排序的。

 

getAcceptableLanguages()處理HTTP請求的Accept-Language頭信息,其中的項已經被解析成一個個java.util.Locale對象。並且和上面的MediaType一樣,已經是根據"q"值有序的了。

 

通過使用@javax.ws.rs.core.Context註釋來注入HttpHeaders對象。例如:

Java代碼  收藏代碼
  1. @Path("/myservice")  
  2. public class MyService {  
  3.   
  4.     @GET  
  5.     public Response get(@Context HttpHeaders headers) {  
  6.         MediaType type = headers.getAcceptableMediaTypes().get(0);  
  7.         Locale language = headers.getAcceptableLanguages().get(0);  
  8.         Object responseObject = ...;  
  9.         Response.ResponseBuilder builder = Response.ok(responseObject, type);  
  10.         builder.language(language);  
  11.         return builder.build();  
  12.     }  
  13.   
  14. }  

 

        > variant處理

 

JAX-RS也提供了API用來處理當你有多個Media Type、Language或Encoding集的情況。通過使用javax.ws.rs.core.Request和javax.ws.rs.core.Variant類來處理這些複雜的匹配。首先看Variant類:

Java代碼  收藏代碼
  1. package javax.ws.rs.core.Variant  
  2. public class Variant {  
  3.   
  4.     public Variant(MediaType mediaType, Locale language, String encoding) {...}  
  5.   
  6.     public Locale getLanguage() {...}  
  7.   
  8.     public MediaType getMediaType() {...}  
  9.   
  10.     public String getEncoding() {...}  
  11.   
  12. }  

 

Variant類就是一個簡單的包含Media Type,Language和Encoding的結構。它表示一個簡單的你JAX-RS資源方法所支持的集合。然後通過在Request接口中設置一列這個類的對象來進行交互:

Java代碼  收藏代碼
  1. package javax.ws.rs.core.Request  
  2. public interface Request {  
  3.   
  4.     Variant selectVariant(List<Variant> variants) throws IllegalArgumentException;  
  5.     ...  
  6.   
  7. }  

其中selectVariant()方法裏設置的就是JAX-RS方法中支持的一列Variant對象。它會檢查請求中的Accept、Accept-Language和Accept-Encoding頭,然後把它們和Variant列表進行比較,找到最匹配的請求的Variant對象。如果沒有符合的對象,則返回null。例如:

Java代碼  收藏代碼
  1. @Path("/myservice")  
  2. public class MyService {  
  3.     @GET  
  4.     Response getSomething(@Context Request request) {  
  5.   
  6.         List<Variant> variants = new ArrayList();  
  7.         variants.add(new Variant(new MediaType("application/xml"),"en""deflate"));  
  8.         variants.add(new Variant(new MediaType("application/xml"),"es""deflate"));  
  9.         variants.add(new Variant(new MediaType("application/json"),"en""deflate"));  
  10.         variants.add(new Variant(new MediaType("application/json"),"es""deflate"));  
  11.         variants.add(new Variant(new MediaType("application/xml"),"en""gzip"));  
  12.         variants.add(new Variant(new MediaType("application/xml"),"es""gzip"));  
  13.         variants.add(new Variant(new MediaType("application/json"),"en""gzip"));  
  14.         variants.add(new Variant(new MediaType("application/json"),"es""gzip"));  
  15.   
  16.         // Pick the variant  
  17.         Variant v = request.selectVariant(variants);  
  18.         Object entity = ...; // get the object you want to return  
  19.         ResponseBuilder builder = Response.ok(entity);  
  20.         builder.type(v.getMediaType()).language(v.getLanguage()).header("Content-Encoding", v.getEncoding());  
  21.   
  22.         return builder.build();  
  23.     }  
  24. }  

 

這裏花了很多代碼去提供所支持的Variant。也有更好的方法去做自動選擇,JAX-RS提供了javax.ws.rs.core.Variant.VariantBuilder類用來創建這些複雜的選擇器:

Java代碼  收藏代碼
  1. public static abstract class VariantListBuilder {  
  2.     public static VariantListBuilder newInstance() {...}  
  3.   
  4.     public abstract VariantListBuilder mediaTypes(MediaType... mediaTypes);  
  5.   
  6.     public abstract VariantListBuilder languages(Locale... languages);  
  7.   
  8.     public abstract VariantListBuilder encodings(String... encodings);  
  9.   
  10.     public abstract List<Variant> build();  
  11.   
  12.     public abstract VariantListBuilder add();  
  13. }  

 

它支持使用Builder的模式來創建variant列表。例如重寫前面的例子:

Java代碼  收藏代碼
  1. @Path("/myservice")  
  2. public class MyService {  
  3.   
  4.     @GET  
  5.     Response getSomething(@Context Request request) {  
  6.         Variant.VariantBuilder vb = Variant.VariantBuilder.newInstance();  
  7.         vb.mediaTypes(new MediaType("application/xml"),  
  8.                 new MediaType("application/json"))  
  9.                 .languages(new Locale("en"), new Locale("es"))  
  10.                 .encodings("deflate""gzip");  
  11.   
  12.         List<Variant> variants = vb.build();  
  13.         // Pick the variant  
  14.         Variant v = request.selectVariant(variants);  
  15.         Object entity = ...; // get the object you want to return  
  16.         ResponseBuilder builder = Response.ok(entity);  
  17.         builder.type(v.getMediaType())  
  18.                 .language(v.getLanguage())  
  19.                 .header("Content-Encoding", v.getEncoding());  
  20.         return builder.build();  
  21.     }  
  22. }  

 

通過調用VariantBuilder的mediaTypes()、languages()和encodings()方法設置可能的值,最後調用build()方法,它會生成一個Variant列表,包含所有可能的組合。

 

VariantBuilder也支持多個不同的Variant集體,通過使用VariantBuilder.add()方法可以分隔和定義不同的Variant集。例如:

 

Java代碼  收藏代碼
  1. Variant.VariantBuilder vb = Variant.VariantBuilder.newInstance();  
  2. vb.mediaTypes(new MediaType("application/xml"),new MediaType("application/json"))  
  3.         .languages(new Locale("en"), new Locale("es"))  
  4.         .encodings("deflate""gzip")  
  5.         .add()  
  6.         .mediaTypes(new MediaType("text/plain"))  
  7.         .languages(new Locale("en"), new Locale("es"), new Locale("fr"))  
  8.         .encodings("compress");  

 

 上例中VariantBuilder創建了兩組Variant,最後builder後就是這兩組的合集。

 

現實中使用Request.selectVariant()方法的例子並不多。首先,content encoding在JAX-RS並不是一個很容易處理的東西,如果你想靈活的處理content encoding,你最好是自己去處理所有的流。大多數JAX-RS的實現應該都自動支持了GZIP。

 

其次,多數JAX-RS的服務都會根據@Produces註釋和Accept頭信息自動處理響應的Media Type。

 

4. URI模式約定

 

Conneg是一個很強大的HTTP特性。問題是有的客戶端,特別是瀏覽器並不支持它。例如Firefox瀏覽器的Accept頭信息被硬編碼爲:

 

Java代碼  收藏代碼
  1. text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  

 

如果你希望訪問的是JSON數據,那可能就失敗了。

 

一個常用的解決此類問題的方法就是把Conneg信息內嵌在URI中,而不是使用Accept頭,例如:

Java代碼  收藏代碼
  1. /customers/en-US/xml/3323  
  2. /customers/3323.xml.en-US  

內容信息以分隔的路徑或文件名後綴被嵌在URI中。上例中,客戶端要求的是XML格式,英文的信息。在JAX-RS中可以如下實現:

 

Java代碼  收藏代碼
  1. @Path("/customers/{id}.{type}.{language}")  
  2. @GET  
  3. public Customer getCustomer(@PathParam("id"int id,  
  4.             @PathParam("type") String type,  
  5.             @PathParam("language") String language) {...}  

 

在JAX-RS規範完成之前,圍繞着文件名後綴的使用確實被定義爲規範的一部分。不幸的是,專業組不同意這個特性的整個語義定義,因此它還是被刪除了。很多JAX-RS的實現仍然支持這個特性,因此瞭解它是怎麼工作的還是很重要的。

 

規範定義和很多JAX-RS實現現在的工作方式是在文件後綴, Media Type和Language之間定義一個映射關係。xml後綴映射到application/xml;en後綴映射到en-US。當一個請求來了,JAX-RS實現就會提取後綴,並使用這個信息作爲Conneg的數據,替換任何傳入的Accept或Accept-Language頭。例如:

Java代碼  收藏代碼
  1. @Path("/customers")  
  2. public class CustomerResource {  
  3.   
  4.     @GET  
  5.     @Produces("application/xml")  
  6.     public Customer getXml() {...}  
  7.   
  8.     @GET  
  9.     @Produces("application/json")  
  10.     public Customer getJson() {...}  
  11.   
  12. }  

如果請求爲 GET /customers.json ,剛JAX-RS實現會提取.json後綴,並把它從請求路徑中移除。然後它會查找匹配json的映射。假設是 application/json ,然後這個信息,而不是Accept頭,不會被使用,最後getJson()方法就會被調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章