2014年3月6日星期四

[]Jersey构建REST服务实战

本文自动转发自我的博客: http://www.haofengjing.org/2289.html

 由于参与移动项目,需要移动终端和服务端传递数据,经过技术对比分析,觉得基于REST模式的Web服务比较简洁易用,于是采取了Jersey开发, 一个Java规范下REST风格Web Service开发框架。最初选择的是Jersey1.8,后来由于运行环境的升级,导致Jersey也要升级到最新版本2.5.1。由于jersey2.x 版本实现的是JAX-RS 2.0规范,与 jersey1.x 相比有很大的改变,JSON转换也有不同。现把实现过程整理下来,供有需要的朋友参考!

一:环境介绍

(1)Jersey2.5.1 

(2)Eclipse 3.7 +Tomcat 7 +JDK 7

二:搭建步骤

(1)通过地址https://jersey.java.net/  下载jaxrs-ri-2.5.1.zip。

(2)通过Eclipse 创建动态工程RestDemo。

(3)解压jaxrs-ri-2.5.1.zip,拷贝下图列表中的JAR包到RestDemo\WebContent\WEB-INF\lib下。

(4)创建包路径  cn.com.vs.vo,创建User.java:

package cn.com.vs.vo;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement public class User { private String name; private String age; private String sex; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } }

(5)创建包路径  cn.com.vs.service,创建RestService.java:

package cn.com.vs.service;

import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType;

import cn.com.vs.vo.User;

@Path("/restService") public class RestService { @GET @Path("/getUserText") @Produces(MediaType.TEXT_PLAIN) public String getUserText() { return "Hello,World!"; } @GET @Path("/getUserXml") @Produces(MediaType.APPLICATION_XML) public User getUserXml() { User user  = new User(); user.setName("snail"); user.setAge("22"); user.setSex("male"); return user; } @GET @Path("/getUserJson") @Produces(MediaType.APPLICATION_JSON) public User getUserJson() { User user  = new User(); user.setName("snail"); user.setAge("22"); user.setSex("male"); return user; } }

(6)在 cn.com.vs 包路径下创建类 RestApplication.java

package cn.com.vs;

import org.glassfish.jersey.filter.LoggingFilter; import org.glassfish.jersey.server.ResourceConfig;

public class RestApplication extends ResourceConfig {

    public RestApplication() { //服务类所在的包路径 packages("cn.com.vs.service"); //打印访问日志,便于跟踪调试,正式发布可清除 register(LoggingFilter.class); }

}

(7)修改web.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance " xmlns="http://java.sun.com/xml/ns/javaee " xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd " xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd " id="WebApp_ID" version="3.0"> <display-name>RestDemo</display-name> <servlet> <servlet-name>mobile</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>cn.com.vs.RestApplication</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>

 <servlet-mapping> <servlet-name>mobile</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>

(8)在 Tomcat7 中部署运行,查看发布效果:

     获取 文本数据:

     获取XML数据:

  获取JSON数据:http://localhost:8080/RestDemo/rest/restService/getUserJson  ,会出现如下异常:

[org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: MessageBodyWriter not found for media type=application/json, type=class cn.com.vs.vo.User, genericType=class cn.com.vs.vo.User.] with root cause org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: MessageBodyWriter not found for media type=application/json, type=class cn.com.vs.vo.User, genericType=class cn.com.vs.vo.User.

说明:如果通过glassfish-4.0运行的话,是没任何问题的!如果通过tomcat,需要添加JSON转换器。

(9)解决获取JSON数据的异常,途径一:

     a)下载 jackson-all-1.9.11.jar  ,放到lib下。

     b)修改RestApplication.java,内容如下:

package cn.com.vs;

import org.codehaus.jackson.jaxrs.JacksonJsonProvider; import org.glassfish.jersey.filter.LoggingFilter; import org.glassfish.jersey.server.ResourceConfig;

public class RestApplication extends ResourceConfig {

    public RestApplication() { //服务类所在的包路径 packages("cn.com.vs.service");  //注册JSON转换器 register(JacksonJsonProvider.class); //打印访问日志,便于跟踪调试,正式发布可清除 register(LoggingFilter.class); }

}

(10)解决获取JSON数据的异常,途径二:

    a) 下载 glassfish-4.0.zip  ,解压,拷贝下列JAR到lib下:

      

   b)修改RestApplication.java,内容如下:

package cn.com.vs;

import org.glassfish.jersey.filter.LoggingFilter; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.server.ResourceConfig;

public class RestApplication extends ResourceConfig {

    public RestApplication() { //服务类所在的包路径 packages("cn.com.vs.service"); //注册JSON转换器 register(JacksonFeature.class);  //打印访问日志,便于跟踪调试,正式发布可清除 register(LoggingFilter.class); }

}

(11)选择上述途径中的一种,然后发布启动,获取JSON数据,即可成功,如下:

 

[]RESTful的Web服务框架Jersey 2.5发布

本文自动转发自我的博客: http://www.haofengjing.org/2288.html
RESTful的Web服务框架Jersey近期发布了2.5版,支持最新版的JettyWeb服务器,升级了Apache Connector,并修正了多个缺陷。Jersey框架基于Java,包括如下改变: 支持Jetty 9 Web服务器和servlet容器。这包括一个基于Jetty HTTP和Servlet容器的Jersey Server容器,以及一个使用Jetty Fluent Client API支持同步和异步客户端调用的Jersey Client连接器。 现在的Apache Connector使用Apache HttpClient 4.3, 这本身就是一个大规模的重构,还包含了很多新的特性和改进。 修复了超过60个bug,包括无法正确读取包含多个值的HTTP头,和通过包扫描缺失组件来进行组件注册。 在6月份主版本2.0发布之后,发生的变化包括: OAuth1可作为消费者或服务提供者,而OAuth2只能作为消费者。 对于每个请求,客户端都可以配置或重写连接器属性。 在使用Grizzly或Apache Connector时支持HTTPS。 支持Spring 3,可以将Spring管理的bean注入到Jersey管理的资源类中,同时可以通过Spring管理JAX-RS资源类。 包括2.5版在内,一共做出了30项改进并修复了超过160个bug。 同时文档也得到了更新,包括从早期2.*迁移到2.5时会产生的问题。JAX-RS 2.0 API规范(JSR 339)的实现参考了Jersey 2.5。Jersey 2.5还是GlassFish应用服务器项目的成员。它取得了两家OSI批准的(开源)许可证书。原文英文链接:RESTful Web Services Framework Jersey 2.5 Released  

[]REST服务开发实战

本文自动转发自我的博客: http://www.haofengjing.org/2286.html

REST介绍

如果要说什么是REST的话,那最好先从Web(万维网)说起。

什么是Web呢?读者可以查看维基百科的词条(http://zh.wikipedia.org/zh-cn/Web),具体的我就不多说了。总之,Web是我们在互联网上最常用的服务,甚至在某些人的心中,互联网就是Web。当然,Web只是互联网的一部分而已,只是大家用的最多而已,我们访问的所有网站都是基于Web。

那么,Web和REST之间究竟有什么关系呢?我们接下来将聊聊组成Web的几大基础技术,URI (统一资源标识符,用来标识资源)、HTTP (超文本传输/转移协议,用来操作资源)、Hypertext (超文本,用来描述资源的内容与状态,我们可以用HTML、XML、JSON或者自定义格式的文本来描述任何一个资源)。

那我们再来看看什么是REST呢?其实REST并不是一种新兴的技术语言,也不是什么新的技术框架。准确来说说REST只是一种概念、风格或者约束,是回归HTTP本身的建议。

REST是由Roy Thomas Fieding在他的博士论文《Architectural Styles and the Design of Network-based Software Architectures》(《架构风格与基于网络的软件架构设计》)中提出的一种架构思想。Roy Fielding是Apache基金会的合作创作者,同时也是HTTP、URI等Web基础协议的主要设计者。从Roy Fielding的背景,我想大家就应该能了解到REST与Web之间的关系了吧。的确,在REST中我们关注技术实际上也只是URI、HTTP、Hypertext而已。

Roy在他的论文中提出了一个RESTful应用应该具备的几点约束。

  • 每个资源都应该有一个唯一的标识
  • 使用标准的方法来更改资源的状态
  • Request和Response的自描述
  • 资源多重表述
  • 无状态的服务

Roy认为,只有具备了上面的约束的应用才能算是REST应用,其实现在许多所谓的REST应用或服务,其实并不能算是真正的REST应用。

我发现,目前很多所谓的REST应用,其实只是RPC而已。出现这样的情况很正常,因为RPC更符合一般程序员的思维。可是,REST和RPC之间还是有很大的差异的,下面我们说一说REST和RPC之间的区别。

  • REST强调资源有唯一的URI;而RPC更加强大过程(动词),由统一的接口来调用它们。
  • REST回归HTTP最初的设计;RPC仅仅只是把HTTP作为传输协议来使用。
  • REST是由超文本驱动的;RPC是由方法驱动的。
  • REST强调HTTP通信的语义可见性,通过消息头和标准的HTTP方法来体现;RPC把语义封装在HTTP消息体中。

REST的应用场景

通过上面的介绍,大家应该对REST有一些最基本的了解,由于REST应用的这些约束,我们可以很轻易地了解和使用REST的服务(只要你了解HTTP)。

其实,我们经常容易犯的一个错误是:当我们了解了一个新的技术,就会用这个技术来解决所有的问题。有一句谚语是这么来说的:"在锤子的眼里,所有的东西都是钉子",其实REST也只是我们工具箱里面的其中的一个工具而已,希望不要把它当做我们唯一的工具。下面,我们就来聊聊适合使用REST的应用场景和不适合使用REST的应用场景。

在我看来,REST最适合的应用场景是需要对外暴露服务的情况。此时,我们可以充分利用REST的自描述、无状态、唯一标识等特性来提供清晰、友好的API;此外,目前Jesery、RESTEasy等JAX-RS框架也提供了OAuth的支持,基本上能够保证服务安全。

最不适合的应用场景是对性能要求高的系统内部的服务调用,在这种情况下使用REST的话,那么REST所有的特性都会变成拖累。这个时候,还是需要选择更底层的通信协议和方式会更好一些,比如ICE。这样的错误,我曾经犯过,后来通过很长时间的努力才慢慢的将这个错误改过来。

规划REST服务

当我们要规划REST服务的时候,其中最关键的概念是"资源"。

资源是什么呢?广义上讲,任何事物只要有用,它就是资源。狭义的讲(在Web环境中),它是一个可以存放、连接在计算机上,可以通过比特流进行操控的实体。一个实体若要成为资源,它必须有一个URI。在这里URI包含了两重含义:1)它是该资源的名称 2)它是该资源的地址。

在规划URI的时候,有几点希望大家能够注意一下:

  • 一个URI标识一个资源,但是一个资源可以被多个URI标识。
  • 资源也是有层次的,这个层次应该在URI上充分的体现出来。
  • 在规划URI的时候,需要定义一些团队内部确认的关键字或符号,这些关键字或符号是有特殊意义的,不能随便使用。
  • 需要有一个URI定义的文档,以备以后的查询和维护。
  • 可以使用URI Template来描述URI的定义。如何使用URI Template请看看这篇文章 。

当我们定义好资源之后,接下来要做的事情就是定义操作资源的方法以及资源的表述格式了。

使用HTTP提供的基本方法来对资源进行操作,一般的操作定义如下:POST(创建资源)、GET(获取资源)、PUT(修改资源)、DELETE(删除)。它们正好对应了CRUD。

对资源的表述,一般的选择会是XML,但是我更加推荐使用JSON来表述资源。在网络中的传输量小,而且也便于JavaScript解析,同时使用其他语言解析也是非常方便的事情。不过,最关键的还是占用更少的资源,让同样的资源能够服务于更多的人。

下面的这张图就很好地说明了REST中最重要的围绕资源的三角关系。

在InfoQ上有一篇很好的文章来介绍如何规划REST服务《如何获取(GET)一杯咖啡——星巴克REST案例分析》,估计大家看完这篇文章,应该对如何规划REST服务会有更深的认识。

选择一个快速方便的REST框架

目前,REST的框架非常多,推荐大家使用Jersey和RESTEasy来创建自己的REST服务。

这两个框架都出自名门,Jersey是由SUN提供的JAX-RS实现参考,对JAX-RS支持的最为充分和快速,基本上所有的JAX-RS的新特性都会在Jersey里第一个体现出来,而且提供了相当齐备的例子让你学习。RESTEasy则是JBoss开源的项目,它同样有很多优点,文档也比Jersey更好一些,但是它和JBoss应用服务器绑定的比较紧密,这点我个人不太喜欢,如果你是熟悉JBoss应用服务器的人,那就选择RESTEasy,它给人的感觉是更加成熟一些,不像Jersey那样频繁地加入新特性。说到底,还是需要根据个人自己的喜好来选择。

如何使用Jersey来快速创建REST应用?参见通过Jersey快速构建REST应用 ;如何使用RESTEasy快速创建REST应用?参见使用RESTEasy快速创建REST应用 。

发布REST服务需要注意的地方

之前,我提到了使用REST的最佳的场景是对外提供公开的服务,也就是所谓的OpenAPI。一旦开放了API,我们就很难控制这些API的使用及其调整了,如果在开放这些API之前考虑的不周到的话,那么后期的维护就会是一个非常麻烦的事情了。所以,当我们决定要开放API的时候,我们一定要注意一些事情,下面的这些算是我的经验总结。

对外暴露API时,需要注意版本规划,以便以后API的升级和维护。在开始开放API的之前,API的版本规划是一件很容易被忽视的工作。但是一旦你的API开放之后,你就会发现,没有对开放的API进行版本规划,是一件非常愚蠢的事情。当使用你的API的人越来越多,当你的开放的API越来越多,一旦某个API要升级,输入和输出发生变化的时候,你更不知道该通知谁来升级,解决问题的时候同样非常麻烦。

另外,由于对外暴露API之后,你很难控制API被调用的次数和意图,所以需要在一些关键的API被调用的次数和频率上进行控制,以免受到恶意攻击。但是,访问次数和频率应该控制在怎样的程度,就要看你的API的重要程度以及负载能力了,每个系统都会有自己的评判,只要你掌握好了这个尺度,应该都不会有问题的。

另外,由于当前浏览器的限制,只能使用HTTP的GET和POST方法,如果通过AJAX直接调用REST服务,当你的服务中需要使用HTTP的PUT或者DELETE方法调用服务时,最好应考虑使用重载POST的方式,将需要使用PUT和DELETE方法调用的服务通过POST方法调用。

关于作者

邓涛(Tony Deng),目前从事移动互联网领域,关注高性能、高并发的互联网架构。


[]基于 REST 的 Web 服务:基础

本文自动转发自我的博客: http://www.haofengjing.org/2284.html

基础

REST 定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的 Web 服务,包括使用不同语言编写的客户端如何通过 HTTP 处理和传输资源状态。 如果考虑使用它的 Web 服务的数量,REST 近年来已经成为最主要的 Web 服务设计模型。 事实上,REST 对 Web 的影响非常大,由于其使用相当方便,已经普遍地取代了基于 SOAP 和 WSDL 的接口设计。

REST 这个概念于 2000 年由 Roy Fielding 在就读加州大学欧文分校期间在学术论文"Architectural Styles and the Design of Network-based Software Architectures"(请参见参考资料以获取此论文的链接)首次提出,他的论文中对使用 Web 服务作为分布式计算平台的一系列软件体系结构原则进行了分析,而其中提出的 REST 概念并没有获得现在这么多关注。 多年以后的今天,REST 的主要框架已经开始出现,但仍然在开发中,因为它已经被广泛接纳到各个平台中,例如通过 JSR-311 成为了 Java™ 6 不可或缺的部分。

本文认为,对于今天正在吸引如此多注意力的最纯粹形式的 REST Web 服务,其具体实现应该遵循四个基本设计原则:

  • 显式地使用 HTTP 方法。
  • 无状态。
  • 公开目录结构式的 URI。
  • 传输 XML、JavaScript Object Notation (JSON),或同时传输这两者。

下面几个部分将详述这四个原则,并提供技术原理解释,说明为什么这些原则对 REST Web 服务设计人员非常重要。

显式地使用 HTTP 方法

基于 REST 的 Web 服务的主要特征之一是以遵循 RFC 2616 定义的协议的方式显式使用 HTTP 方法。例如,HTTP GET 被定义为数据产生方法,旨在由客户端应用程序用于检索资源以从 Web 服务器获取数据,或者执行某个查询并预期 Web 服务器将查找某一组匹配资源然后使用该资源进行响应。

REST 要求开发人员显式地使用 HTTP 方法,并且使用方式与协议定义一致。 这个基本 REST 设计原则建立了创建、读取、更新和删除(create, read, update, and delete,CRUD)操作与 HTTP 方法之间的一对一映射。 根据此映射:

  • 若要在服务器上创建资源,应该使用 POST 方法。
  • 若要检索某个资源,应该使用 GET 方法。
  • 若要更改资源状态或对其进行更新,应该使用 PUT 方法。
  • 若要删除某个资源,应该使用 DELETE 方法。

许多 Web API 中所固有的一个令人遗憾的设计缺陷在于将 HTTP 方法用于非预期用途。 例如,HTTP GET 请求中的请求 URI 通常标识一个特定的资源。 或者,请求 URI 中的查询字符串包括一组参数,这些参数定义服务器用于查找一组匹配资源的搜索条件。 至少,HTTP/1.1 RFC 是这样描述 GET 方法的。 但是在许多情况下,不优雅的 Web API 使用 HTTP GET 来触发服务器上的事务性操作——例如,向数据库添加记录。 在这些情况下,GET 请求 URI 属于不正确使用,或者至少不是以基于 REST 的方式使用。 如果 Web API 使用 GET 调用远程过程,则应该类似如下: GET /adduser?name=Robert HTTP/1.1

这不是非常优雅的设计,因为上面的 Web 方法支持通过 HTTP GET 进行状态更改操作。 换句话说,该 HTTP GET 请求具有副作用。 如果处理成功,则该请求的结果是向基础数据存储区添加一个新用户——在此例中为 Robert。 这里的问题主要在语义上。 Web 服务器旨在通过检索与请求 URI 中的路径(或查询条件)匹配的资源,并在响应中返回这些资源或其表示形式,从而响应 HTTP GET 请求,而不是向数据库添加记录。 从该协议方法的预期用途的角度看,然后再从与 HTTP/1.1 兼容的 Web 服务器的角度看,以这种方式使用 GET 是不一致的。

除了语义之外,GET 的其他问题在于,为了触发数据库中的记录的删除、修改或添加,或者以某种方式更改服务器端状态,它请求 Web 缓存工具(爬网程序)和搜索引擎简单地通过对某个链接进行爬网处理,从而意外地做出服务器端更改。 克服此常见问题的简单方法是将请求 URI 上的参数名称和值转移到 XML 标记中。 这样产生的标记是要创建的实体的 XML 表示形式,可以在 HTTP POST 的正文中进行发送,此 HTTP POST 的请求 URI 是该实体的预期父实体(请参见清单 1 和 2):

清单 1. 之前
GET /adduser?name=Robert HTTP/1.1
清单 2. 之后
POST /users HTTP/1.1  Host: myserver  Content-Type: application/xml  <?xml version="1.0"?>  <user>    <name>Robert</name>  </user>

上述方法是基于 REST 的请求的范例: 正确使用 HTTP POST 并将有效负载包括在请求的正文中。 在接收端,可以通过将正文中包含的资源添加为请求 URI 中标识的资源的从属资源,从而处理该请求;在此例下,应该将新资源添加为 /users 的子项。 POST 请求中指定的这种新实体与其父实体之间的包含关系类似于某个文件从属于其父目录的方式。 客户端设置实体与其父实体之间的关系,并在 POST 请求中定义新实体的 URI。

然后客户端应用程序可以使用新的 URI 获取资源的表示形式,并至少逻辑地指明该资源位于 /users 之下,如清单 3 所示。

清单 3. HTTP GET 请求
GET /users/Robert HTTP/1.1  Host: myserver  Accept: application/xml

以这种方式使用 GET 是显式的,因为 GET 仅用于数据检索。 GET 是应该没有副作用的操作,即所谓的等幂性 属性。

当支持通过 HTTP GET 执行更新操作时,也需要应用类似的 Web 方法重构,如清单 4 所示。

清单 4. 通过 HTTP GET 进行更新
GET /updateuser?name=Robert&newname=Bob HTTP/1.1

这更改了资源的 name 特性(或属性)。 虽然可以将查询字符串用于此类操作,清单 4 就是一个简单的例子,但是在用于较复杂的操作时,这种将查询字符串作为方法签名的模式往往会崩溃。 由于您的目标是显式使用 HTTP 方法,鉴于上述的相同原因(请参见清单 5),更符合 REST 的方法是发送 HTTP PUT 请求以更新资源,而不是发送 HTTP GET。

清单 5. HTTP PUT 请求
PUT /users/Robert HTTP/1.1  Host: myserver  Content-Type: application/xml  <?xml version="1.0"?>  <user>    <name>Bob</name>  </user>

使用 PUT 取代原始资源可以提供更清洁的接口,这样的接口与 REST 的原则以及与 HTTP 方法的定义一致。 清单 5 中的 PUT 请求是显式的,因为它通过在请求 URI 中标识要更新的资源来指向该资源,并且它在 PUT 请求的正文中将资源的新表示形式从客户端传输到服务器,而不是在请求 URI 上将资源属性作为参数名称和值的松散集合进行传输。 清单 5 还具有将资源从 Robert 重命名为 Bob 的效果,这样做会将其 URI 更改为 /users/Bob。 在 REST Web 服务中,使用旧的 URI 针对该资源的后续请求会产生标准的 404 Not Found 错误。

作为一般设计原则,通过在 URI 中使用名词而不是动词,对于遵循有关显式使用 HTTP 方法的 REST 指导原则是有帮助的。 在基于 REST 的 Web 服务中,协议已经对动词(POST、GET、PUT 和 DELETE)进行了定义。 在理想的情况下,为了保持接口的通用化,并允许客户端明确它们调用的操作,Web 服务不应该定义更多的动词或远程过程,例如 /adduser 或 /updateuser。 这条通用设计原则也适用于 HTTP 请求的正文,后者旨在用于传输资源状态,而不是用于携带要调用的远程方法或远程过程的名称。

无状态

REST Web 服务需要扩展以满足日益提高的性能要求。 具有负载平衡和故障转移功能、代理和网关的服务器集群通常以形成服务拓扑的方式进行组织,从而允许根据需要将请求从一个服务器路由到另一个服务器,以减少 Web 服务调用的总体响应时间。 要使用中间服务器扩大规模,REST Web 服务需要发送完整、独立的请求;也就是说,发送的请求包括所有需要满足的数据,以便中间服务器中的组件能够进行转发、路由和负载平衡,而不需要在请求之间在本地保存任何状态。

完整、独立的请求不要求服务器在处理请求时检索任何类型的应用程序上下文或状态。 REST Web 服务应用程序(或客户端)在 HTTP Header 和请求正文中包括服务器端组件生成响应所需要的所有参数、上下文和数据。 这种意义上的无状态可以改进 Web 服务性能,并简化服务器端组件的设计和实现,因为服务器上没有状态,从而消除了与外部应用程序同步会话数据的需要。

图 1 演示了一个有状态的服务,某个应用程序可能向其请求多页结果集中的下一个页面,并假设该服务跟踪应用程序在结果集中导航时的离开位置。 在这个有状态的设计中,该服务递增并在某个位置存储 previousPage 变量,以便能够响应针对下一个页面的请求。

图 1. 有状态的设计
有状态的设计

类似如此的有状态的服务变得复杂化了。 在 Java Platform, Enterprise Edition (Java EE) 环境中,有状态的服务需要大量的预先考虑,以高效地存储会话数据和支持整个 Java EE 容器集群中的会话数据同步。 在此类环境中,存在一个 Servlet/JavaServer Pages (JSP) 和 Enterprise JavaBeans (EJB) 开发人员非常熟悉的问题,他们经常在会话复制过程中艰难地查找引发 java.io.NotSerializableException 的根源。 无论该异常是由 Servlet 容器在 HttpSession 复制过程中引发的,还是由 EJB 容器在有状态的 EJB 复制过程中引发的,这都是个问题,会耗费开发人员几天的时间,尝试在构成服务器状态并且有时非常复杂的对象图表中查明没有实现 Serializable 的对象。 此外,会话同步增加了开销,从而影响服务器性能。

另一方面,无状态的服务器端组件不那么复杂,很容易跨进行负载平衡的服务器进行设计、编写和分布。 无状态的服务不仅性能更好,而且还将大部分状态维护职责转移给客户端应用程序。 在基于 REST 的 Web 服务中,服务器负责生成响应,并提供使客户端能够独自维护应用程序状态的接口。 例如,在针对多页结果集的请求中,客户端应该包括要检索的实际页编号,而不是简单地要求检索下一页(请参见图 2)。

图 2. 无状态的设计
无状态的设计

无状态的 Web 服务生成的响应链接到结果集中的下一个页编号,并允许客户端完成所需的相关工作以便保留此值。 可以作为大致的分离将基于 REST 的 Web 服务设计的这个方面划分为两组职责,以阐明如何维护无状态的服务:

服务器

  • 生成响应,其中包括指向其他资源的链接,以使得应用程序可以在相关资源之间导航。 此类响应嵌入了链接。 类似地,如果请求是针对父或容器资源,则基于 REST 的典型响应还可能包括指向父资源的子资源或从属资源的链接,以便这些资源保持连接在一起。
  • 生成响应,其中指明了是否可缓存,以通过减少针对重复资源的请求数量或通过完全消除某些请求来改进性能。 服务器通过包括 Cache-Control 和 Last-Modified(日期值)HTTP 响应 Header 实现此目的。

客户端应用程序

  • 使用 Cache-Control 响应 Header 确定是否缓存资源(创建资源的本地副本)。 客户端还读取 Last-Modified 响应 Header,并在 If-Modified-Since Header 中发回日期值,以向服务器询问资源是否已更改。 这称为条件 GET (Conditional GET),两个 Header 同时进行,因为服务器的响应为标准 304 代码 (Not Modified),如果请求的资源自从该时间以后尚未更改,则省略实际的资源。 HTTP 响应代码 304 意味着客户端可以安全地将资源表示形式的缓存本地副本作为最新版本使用,从而实际上跳过了后续 GET 请求,直到资源更改为止。
  • 发送可独立于其他请求得到服务的完整请求。 这要求客户端充分利用 Web 服务接口指定的 HTTP Header,并在请求正文中发送完整的资源表示形式。 客户端发送的请求极少对先前的请求、某个会话在服务器上的存在性、服务器向请求添加上下文的能力或请求之间保留的应用程序状态做出假设。

客户端应用程序与服务之间的这种协作对于基于 REST 的 Web 服务中的无状态性极为重要。 它通过节省带宽和最小化服务器端应用程序状态改进了性能。

公开目录结构式的 URI

从对资源寻址的客户端应用程序的角度看,URI 决定了 REST Web 服务将具有的直观程度,以及服务是否将以设计人员能够预测的方式被使用。 基于 REST 的 Web 服务的第三个特征完全与 URI 相关。

REST Web 服务 URI 的直观性应该达到很容易猜测的程度。 将 URI 看作是自身配备文档说明的接口,开发人员只需很少(如果有的话)的解释或参考资料即可了解它指向什么,并获得相关的资源。 为此,URI 的结构应该简单、可预测且易于理解。

实现这种级别的可用性的方法之一是定义目录结构式的 URI。 此类 URI 具有层次结构,其根为单个路径,从根开始分支的是公开服务的主要方面的子路径。 根据此定义,URI 并不只是斜杠分隔的字符串,而是具有在节点上连接在一起的下级和上级分支的树。 例如,在一个收集从 Java 到报纸的各种主题的讨论线程服务中,您可能定义类似如下的结构化 URI 集合: http://www.myservice.org/discussion/topics/{topic}

根 /discussion 之下有一个 /topics 节点。 该节点之下有一系列主题名称,例如闲谈、技术等等,每个主题名称指向某个讨论线程。 在此结构中,只需在 /topics/ 后面输入某个内容即可容易地收集讨论线程。

在某些情况下,指向资源的路径尤其适合于目录式结构。 例如,以按日期进行组织的资源为例,这种资源非常适合于使用层次结构语法。

此示例非常直观,因为它基于规则: http://www.myservice.org/discussion/2008/12/10/{topic}

第一个路径片段是四个数字的年份,第二个路径片断是两个数字的日期,第三个片段是两个数字的月份。 这样解释它可能有点愚蠢,但这就是我们追求的简单级别。 人类和计算机能够容易地生成类似如此的结构化 URI,因为这些 URI 基于规则。 在语法的空隙中填入路径部分就大功告成了,因为存在用于组合 URI 的明确模式: http://www.myservice.org/discussion/{year}/{day}/{month}/{topic}

在考虑基于 REST 的 Web 服务的 URI 结构时,需要指出的一些附加指导原则包括:

  • 隐藏服务器端脚本技术文件扩展名(.jsp、.php、.asp)——如果有的话,以便您能够移植到其他脚本技术而不用更改 URI。
  • 将所有内容保持小写。
  • 将空格替换为连字符或下划线(其中一种或另一种)。
  • 尽可能多地避免查询字符串。
  • 如果请求 URI 用于部分路径,与使用 404 Not Found 代码不同,应该始终提供缺省页面或资源作为响应。

URI 还应该是静态的,以便在资源发生更改或服务的实现发生更改时,链接保持不变。 这可以实现书签功能。 URI 中编码的资源之间的关系与在存储资源的位置表示资源关系的方式无关也是非常重要的。

传输 XML、JSON 或同时传输这两者

资源表示形式通常反映了在客户端应用程序请求资源时的资源当前状态及其属性。 这种意义上的资源表示形式只是时间上的快照。 这可以像数据库中的记录表示形式一样简单,其中包括列名称与 XML 标记之间的映射,XML 中的元素值包含行值。 或者,如果系统具有数据模型,那么根据此定义,资源表示形式是系统的数据模型中的对象之一的属性快照。 这些对象就是您希望您的 REST Web 服务为客户端提供的资源。

基于 REST 的 Web 服务设计中的最后一组约束与应用程序和服务在请求/响应有效负载或 HTTP 正文中交换的数据的格式有关。 这是真正值得将一切保持简单、可读和连接在一起的方面。

数据模型中的对象通常以某种方式相关,应该以在将资源传输到客户端应用程序时表示资源的方式,反映数据模型对象(资源)之间的关系。 在讨论线程服务中,连接的资源表示形式的示例可能包括根讨论主题及其属性,以及指向为该主题提供的响应的嵌入链接。

清单 6. 讨论线程的 XML 表示形式
<?xml version="1.0"?>  <discussion date="{date}" topic="{topic}">    <comment>{comment}</comment>    <replies>      <reply from="joe@mail.com" href="/discussion/topics/{topic}/joe"/>      <reply from="bob@mail.com" href="/discussion/topics/{topic}/bob"/>    </replies>  </discussion>

最后,为了赋予客户端请求最适合它们的特定内容类型的能力,您的服务的构造应该利用内置的 HTTP Accept Header,其中该 Header 的值为 MIME 类型。 基于 REST 的服务使用的一些常见 MIME 类型如表 1 所示。

表 1. 基于 REST 的服务使用的常见 MIME 类型
MIME-Type Content-Type
JSON application/json
XML application/xml
XHTML application/xhtml+xml

这使得服务可由运行在不同平台和设备上并采用不同语言编写的各种各样的客户端所使用。 使用 MIME 类型和 HTTP Accept Header 是一种称为内容协商 的机制,这种机制允许客户端选择适合于它们的数据格式,并最小化服务与使用服务的应用程序之间的数据耦合。

结束语

REST 并非始终是正确的选择。 它作为一种设计 Web 服务的方法而变得流行,这种方法对专有中间件(例如某个应用程序服务器)的依赖比基于 SOAP 和 WSDL 的方法更少。 在某种意义上,通过强调 URI 和 HTTP 等早期 Internet 标准,REST 是对大型应用程序服务器时代之前的 Web 方式的回归。 正如您已经在所谓的基于 REST 的接口设计原则中研究过的一样,XML over HTTP 是一个功能强大的接口,允许内部应用程序(例如基于 Asynchronous JavaScript + XML (Ajax) 的自定义用户界面)轻松连接、定位和使用资源。 事实上,Ajax 与 REST 之间的完美配合已增加了当今人们对 REST 的注意力。

通过基于 REST 的 API 公开系统资源是一种灵活的方法,可以为不同种类的应用程序提供以标准方式格式化的数据。 它可以帮助满足集成需求(这对于构建可在其中容易地组合 (Mashup) 数据的系统非常关键),并帮助将基于 REST 的基本服务集扩展或构建为更大的集合。 本文仅略微谈到了基础,但愿本文的讨论会诱发您继续探索该主题。


[]使用 JAX-RS 简化 REST 应用开发

本文自动转发自我的博客: http://www.haofengjing.org/2283.html

REST 简介

REST 是英文 Representational State Transfer 的缩写,有中文翻译为"具象状态传输"。REST 这个术语是由 Roy Fielding 在他的博士论文 《 Architectural Styles and the Design of Network-based Software Architectures 》中提出的。REST 并非标准,而是一种开发 Web 应用的架构风格,可以将其理解为一种设计模式。REST 基于 HTTP,URI,以及 XML 这些现有的广泛流行的协议和标准,伴随着 REST,HTTP 协议得到了更加正确的使用。

相较于基于 SOAP 和 WSDL 的 Web 服务,REST 模式提供了更为简洁的实现方案。目前,越来越多的 Web 服务开始采用 REST 风格设计和实现,真实世界中比较著名的 REST 服务包括:Google AJAX 搜索 API 、Amazon Simple Storage Service (Amazon S3) 等。

基于 REST 的 Web 服务遵循一些基本的设计原则:

  • 系统中的每一个对象或是资源都可以通过一个唯一的 URI 来进行寻址,URI 的结构应该简单、可预测且易于理解,比如定义目录结构式的 URI。
  • 以遵循 RFC-2616 所定义的协议的方式显式地使用 HTTP 方法,建立创建、检索、更新和删除(CRUD:Create, Retrieve, Update and Delete)操作与 HTTP 方法之间的一对一映射:
    • 若要在服务器上创建资源,应该使用 POST 方法;
    • 若要检索某个资源,应该使用 GET 方法;
    • 若要更改资源状态或对其进行更新,应该使用 PUT 方法;
    • 若要删除某个资源,应该使用 DELETE 方法。
  • URI 所访问的每个资源都可以使用不同的形式加以表示(比如 XML 或者 JSON),具体的表现形式取决于访问资源的客户端,客户端与服务提供者使用一种内容协商的机制(请求头与 MIME 类型)来选择合适的数据格式,最小化彼此之间的数据耦合。

JAX-RS -- Java API for RESTful Web Services

Java EE 6 引入了对 JSR-311 的支持。JSR-311(JAX-RS:Java API for RESTful Web Services)旨在定义一个统一的规范,使得 Java 程序员可以使用一套固定的接口来开发 REST 应用,避免了依赖于第三方框架。同时,JAX-RS 使用 POJO 编程模型和基于标注的配置,并集成了 JAXB,从而可以有效缩短 REST 应用的开发周期。

JAX-RS 定义的 API 位于 javax.ws.rs 包中,其中一些主要的接口、标注和抽象类如 图 1所示。

图 1. javax.ws.rs 包概况
图 1. javax.ws.rs 包概况

JAX-RS 的具体实现由第三方提供,例如 Sun 的参考实现 Jersey、Apache 的 CXF 以及 JBoss 的 RESTEasy。

在接下来的文章中,将结合一个记账簿应用向读者介绍 JAX-RS 一些关键的细节。

示例简介

记账簿示例应用程序中包含了 3 种资源:账目、用户以及账目种类,用户与账目、账目种类与账目之间都是一对多的关系。记账簿实现的主要功能包括:

  1. 记录某用户在什么时间花费了多少金额在哪个种类上
  2. 按照用户、账目种类、时间或者金额查询记录
  3. 对用户以及账目种类的管理

Resource 类和 Resource 方法

Web 资源作为一个 Resource 类来实现,对资源的请求由 Resource 方法来处理。Resource 类或 Resource 方法被打上了 Path 标注,Path 标注的值是一个相对的 URI 路径,用于对资源进行定位,路径中可以包含任意的正则表达式以匹配资源。和大多数 JAX-RS 标注一样,Path 标注是可继承的,子类或实现类可以继承超类或接口中的 Path 标注。

Resource 类是 POJO,使用 JAX-RS 标注来实现相应的 Web 资源。Resource 类分为根 Resource 类和子 Resource 类,区别在于子 Resource 类没有打在类上的 Path 标注。Resource 类的实例方法打上了 Path 标注,则为 Resource 方法或子 Resource 定位器,区别在于子 Resource 定位器上没有任何 @GET、@POST、@PUT、@DELETE 或者自定义的 @HttpMethod。清单 1展示了示例应用中使用的根 Resource 类及其 Resource 方法。

清单 1. 根 Resource 类
 @Path("/")    public class BookkeepingService {       ......       @Path("/person/")       @POST       @Consumes("application/json")       public Response createPerson(Person person) {           ......       }         @Path("/person/")       @PUT       @Consumes("application/json")       public Response updatePerson(Person person) {           ......       }         @Path("/person/{id:\\d+}/")       @DELETE       public Response deletePerson(@PathParam("id")       int id) {           ......       }         @Path("/person/{id:\\d+}/")       @GET       @Produces("application/json")       public Person readPerson(@PathParam("id")       int id) {           ......       }         @Path("/persons/")       @GET       @Produces("application/json")       public Person[] readAllPersons() {           ......       }         @Path("/person/{name}/")       @GET       @Produces("application/json")       public Person readPersonByName(@PathParam("name")       String name) {           ......    }    ......

参数标注

JAX-RS 中涉及 Resource 方法参数的标注包括:@PathParam、@MatrixParam、@QueryParam、@FormParam、@HeaderParam、@CookieParam、@DefaultValue 和 @Encoded。这其中最常用的是 @PathParam,它用于将 @Path 中的模板变量映射到方法参数,模板变量支持使用正则表达式,变量名与正则表达式之间用分号分隔。例如对 清单 1中所示的 BookkeepingService 类,如果使用 Get 方法请求资源"/person/jeffyin",则 readPersonByName 方法将被调用,方法参数 name 被赋值为"jeffyin";而如果使用 Get 方法请求资源"/person/123",则 readPerson 方法将被调用,方法参数 id 被赋值为 123。要了解如何使用其它的参数标注 , 请参考 JAX-RS API

JAX-RS 规定 Resource 方法中只允许有一个参数没有打上任何的参数标注,该参数称为实体参数,用于映射请求体。例如 清单 1 中所示的 BookkeepingService 类的 createPerson 方法和 updatePerson 方法的参数 person。

参数与返回值类型

Resource 方法合法的参数类型包括:

  1. 原生类型
  2. 构造函数接收单个字符串参数或者包含接收单个字符串参数的静态方法 valueOf 的任意类型
  3. List<T>,Set<T>,SortedSet<T>(T 为以上的 2 种类型)
  4. 用于映射请求体的实体参数

Resource 方法合法的返回值类型包括:

  1. void:状态码 204 和空响应体
  2. Response:Response 的 status 属性指定了状态码,entity 属性映射为响应体
  3. GenericEntity:GenericEntity 的 entity 属性映射为响应体,entity 属性为空则状态码为 204,非空则状态码为 200
  4. 其它类型:返回的对象实例映射为响应体,实例为空则状态码为 204,非空则状态码为 200

对于错误处理,Resource 方法可以抛出非受控异常 WebApplicationException 或者返回包含了适当的错误码集合的 Response 对象。

Context 标注

通过 Context 标注,根 Resource 类的实例字段可以被注入如下类型的上下文资源:

  1. Request、UriInfo、HttpHeaders、Providers、SecurityContext
  2. HttpServletRequest、HttpServletResponse、ServletContext、ServletConfig

要了解如何使用第 1 种类型的上下文资源 , 请参考 JAX-RS API

CRUD 操作

JAX-RS 定义了 @POST、@GET、@PUT 和 @DELETE,分别对应 4 种 HTTP 方法,用于对资源进行创建、检索、更新和删除的操作。

POST 标注

POST 标注用于在服务器上创建资源,如 清单 2所示。

清单 2. POST 标注
 @Path("/")    public class BookkeepingService {       ......       @Path("/account/")       @POST       @Consumes("application/json")       public Response createAccount(Account account) {           ......       }    ......

如果使用 POST 方法请求资源"/account",则 createAccount 方法将被调用,JSON 格式的请求体被自动映射为实体参数 account。

GET 标注

GET 标注用于在服务器上检索资源,如 清单 3所示。

清单 3. GET 标注
 @Path("/")    public class BookkeepingService {       ......       @Path("/person/{id}/accounts/")       @GET       @Produces("application/json")       public Account[] readAccountsByPerson(@PathParam("id")       int id) {           ......       }       ......       @Path("/accounts/{beginDate:\\d{4}-\\d{2}-\\d{2}},{endDate:\\d{4}-\\d{2}-\\d{2}}/")       @GET       @Produces("application/json")       public Account[] readAccountsByDateBetween(@PathParam("beginDate")       String beginDate, @PathParam("endDate")       String endDate) throws ParseException {           ......       }    ......

如果使用 GET 方法请求资源"/person/123/accounts",则 readAccountsByPerson 方法将被调用,方法参数 id 被赋值为 123,Account 数组类型的返回值被自动映射为 JSON 格式的响应体;而如果使用 GET 方法请求资源"/accounts/2008-01-01,2009-01-01",则 readAccountsByDateBetween 方法将被调用,方法参数 beginDate 被赋值为"2008-01-01",endDate 被赋值为"2009-01-01",Account 数组类型的返回值被自动映射为 JSON 格式的响应体。

PUT 标注

PUT 标注用于更新服务器上的资源,如 清单 4所示。

清单 4. PUT 标注
 @Path("/")    public class BookkeepingService {       ......       @Path("/account/")       @PUT       @Consumes("application/json")       public Response updateAccount(Account account) {           ......       }    ......

如果使用 PUT 方法请求资源"/account",则 updateAccount 方法将被调用,JSON 格式的请求体被自动映射为实体参数 account。

DELETE 标注

DELETE 标注用于删除服务器上的资源,如 清单 5所示。

清单 5. DELETE 标注
 @Path("/")    public class BookkeepingService {       ......       @Path("/account/{id:\\d+}/")       @DELETE       public Response deleteAccount(@PathParam("id")       int id) {           ......       }    ......

如果使用 DELETE 方法请求资源"/account/323",则 deleteAccount 方法将被调用,方法参数 id 被赋值为 323。

内容协商与数据绑定

Web 资源可以有不同的表现形式,服务端与客户端之间需要一种称为内容协商(Content Negotiation)的机制:作为服务端,Resource 方法的 Produces 标注用于指定响应体的数据格式(MIME 类型),Consumes 标注用于指定请求体的数据格式;作为客户端,Accept 请求头用于选择响应体的数据格式,Content-Type 请求头用于标识请求体的数据格式。

JAX-RS 依赖于 MessageBodyReader 和 MessageBodyWriter 的实现来自动完成返回值到响应体的序列化以及请求体到实体参数的反序列化工作,其中,XML 格式的请求/响应数据与 Java 对象的自动绑定依赖于 JAXB 的实现。

用户可以使用 Provider 标注来注册使用自定义的 MessageBodyProvider,如 清单 6所示,GsonProvider 类使用了 Google Gson 作为 JSON 格式的 MessageBodyProvider 的实现。

清单 6. GsonProvider
 @Provider    @Produces("application/json")    @Consumes("application/json")    public class GsonProvider implements MessageBodyWriter<Object>,       MessageBodyReader<Object> {         private final Gson gson;         public GsonProvider() {           gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().setDateFormat(                   "yyyy-MM-dd").create();       }         public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations,               MediaType mediaType) {           return true;       }         public Object readFrom(Class<Object> type, Type genericType,               Annotation[] annotations, MediaType mediaType,               MultivaluedMap<String, String> httpHeaders, InputStream entityStream)               throws IOException, WebApplicationException {           return gson.fromJson(new InputStreamReader(entityStream, "UTF-8"), type);       }         public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,               MediaType mediaType) {           return true;       }         public long getSize(Object obj, Class<?> type, Type genericType,               Annotation[] annotations, MediaType mediaType) {           return -1;       }         public void writeTo(Object obj, Class<?> type, Type genericType,               Annotation[] annotations, MediaType mediaType,               MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)               throws IOException, WebApplicationException {           entityStream.write(gson.toJson(obj, type).getBytes("UTF-8"));       }      }

JAX-RS 与 JPA 的结合使用

由于 JAX-RS 和 JPA 同样都使用了基于 POJO 和标注的编程模型,因而很易于结合在一起使用。示例应用中的 Web 资源 ( 如账目 ) 同时也是持久化到数据库中的实体,同一个 POJO 类上既有 JAXB 的标注,也有 JPA 的标注 ( 或者还有 Gson 的标注 ) ,这使得应用中类的个数得以减少。如 清单 7所示,Account 类可以在 JAX-RS 与 JPA 之间得到复用,它不但可以被 JAX-RS 绑定为请求体 / 响应体的 XML/JSON 数据,也可以被 JPA 持久化到关系型数据库中。

清单 7. Account
 @Entity    @Table(name = "TABLE_ACCOUNT")    @XmlRootElement    public class Account {       @Id       @GeneratedValue(strategy = GenerationType.IDENTITY)       @Column(name = "COL_ID")       @Expose       private int id;         @ManyToOne       @JoinColumn(name = "COL_PERSON")       @Expose       private Person person;         @Column(name = "COL_AMOUNT")       @Expose       private BigDecimal amount;         @Column(name = "COL_DATE")       @Expose       private Date date;         @ManyToOne       @JoinColumn(name = "COL_CATEGORY")       @Expose       private Category category;         @Column(name = "COL_COMMENT")       @Expose       private String comment;    ......

结束语

REST 作为一种轻量级的 Web 服务架构被越来越多的开发者所采用,JAX-RS 的发布则规范了 REST 应用开发的接口。本文首先阐述了 REST 架构的基本设计原则,然后通过一个示例应用展示了 JAX-RS 是如何通过各种标注来实现以上的设计原则的,最后还介绍了 JAX-RS 与 JPA、Gson 的结合使用。本文的示例应用使用了 Jersey 和 OpenJPA,部署在 Tomcat 容器上,替换成其它的实现只需要修改 web.xml 和 persistence.xml 配置文件。