JAX-RS與Conneg
JAX-RS有一些用來幫助用戶管理他的conneg的工具:
- 基於Accept頭的方法派發
- 允許直接查看內容信息
- 用於處理多約束條件的APIs
1. 方法派發
前幾節中,我們看到怎麼使用@Produces註釋來指示響應的Media Type。 JAX-RS也使用這個註釋來分發請求到恰當的方法上,通過匹配最佳的請求的Accept頭信息的Media Type 列表到由@Produces標註的元數據。例如:
- @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) {...}
- }
這裏三個方法對應的服務路徑一樣,但是@Produces的元數據不同。JAX-RS會基於請求信息的Accept頭信息來選擇恰當的處理方法,例如:
- GET http://example.com/customers/1
- Accept: application/json;q=1.0, application/xml;q=0.5
根據頭一節的介紹,這裏,Accept的數據類型的優先級爲:
- application/json
- application/xml
因此對這個請求,getCustomerJson()方法將被調用。
2. JAXB與Conneg
之前的章節裏介紹了怎麼通過使用JAXB來實現從java對象到xml或者json的映射。而通過在JAX-RS中使用conneg,我們也可以實現一個可以服務於這兩種格式的方法,例如:
- @Path("/service")
- public class MyService {
- @GET
- @Produces({"application/xml", "application/json"})
- public Customer getCustomer(@PathParam("id") int id) {...}
- }
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信息:
- public interface HttpHeaders {
- public List<MediaType> getAcceptableMediaTypes();
- public List<Locale> getAcceptableLanguages();
- ...
- }
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對象。例如:
- @Path("/myservice")
- public class MyService {
- @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();
- }
- }
> variant處理
JAX-RS也提供了API用來處理當你有多個Media Type、Language或Encoding集的情況。通過使用javax.ws.rs.core.Request和javax.ws.rs.core.Variant類來處理這些複雜的匹配。首先看Variant類:
- package javax.ws.rs.core.Variant
- public class Variant {
- public Variant(MediaType mediaType, Locale language, String encoding) {...}
- public Locale getLanguage() {...}
- public MediaType getMediaType() {...}
- public String getEncoding() {...}
- }
Variant類就是一個簡單的包含Media Type,Language和Encoding的結構。它表示一個簡單的你JAX-RS資源方法所支持的集合。然後通過在Request接口中設置一列這個類的對象來進行交互:
- package javax.ws.rs.core.Request
- public interface Request {
- Variant selectVariant(List<Variant> variants) throws IllegalArgumentException;
- ...
- }
其中selectVariant()方法裏設置的就是JAX-RS方法中支持的一列Variant對象。它會檢查請求中的Accept、Accept-Language和Accept-Encoding頭,然後把它們和Variant列表進行比較,找到最匹配的請求的Variant對象。如果沒有符合的對象,則返回null。例如:
- @Path("/myservice")
- public class MyService {
- @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();
- }
- }
這裏花了很多代碼去提供所支持的Variant。也有更好的方法去做自動選擇,JAX-RS提供了javax.ws.rs.core.Variant.VariantBuilder類用來創建這些複雜的選擇器:
- public static abstract class VariantListBuilder {
- public static VariantListBuilder newInstance() {...}
- public abstract VariantListBuilder mediaTypes(MediaType... mediaTypes);
- public abstract VariantListBuilder languages(Locale... languages);
- public abstract VariantListBuilder encodings(String... encodings);
- public abstract List<Variant> build();
- public abstract VariantListBuilder add();
- }
它支持使用Builder的模式來創建variant列表。例如重寫前面的例子:
- @Path("/myservice")
- public class MyService {
- @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();
- // 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的mediaTypes()、languages()和encodings()方法設置可能的值,最後調用build()方法,它會生成一個Variant列表,包含所有可能的組合。
VariantBuilder也支持多個不同的Variant集體,通過使用VariantBuilder.add()方法可以分隔和定義不同的Variant集。例如:
- 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")
- .add()
- .mediaTypes(new MediaType("text/plain"))
- .languages(new Locale("en"), new Locale("es"), new Locale("fr"))
- .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頭信息被硬編碼爲:
- text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
如果你希望訪問的是JSON數據,那可能就失敗了。
一個常用的解決此類問題的方法就是把Conneg信息內嵌在URI中,而不是使用Accept頭,例如:
- /customers/en-US/xml/3323
- /customers/3323.xml.en-US
內容信息以分隔的路徑或文件名後綴被嵌在URI中。上例中,客戶端要求的是XML格式,英文的信息。在JAX-RS中可以如下實現:
- @Path("/customers/{id}.{type}.{language}")
- @GET
- public Customer getCustomer(@PathParam("id") int id,
- @PathParam("type") String type,
- @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頭。例如:
- @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()方法就會被調用。