RESTful和JAX-RS
一、简介
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