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()方法就会被调用。

你可能感兴趣的:(JAX-RS入门,内容约定(2),九:)