一说到REST,很多人的第一反应就是其是前端请求后台的一种通信方式。甚至有些人将REST和RPC混为一谈,认为两者都是基于HTTP的类似的东西。实际上,很少人能详细讲述REST所提出的各个约束、风格特点,以及如何搭建REST服务。
本节,我们将对REST,尤其是如何构建基于REST风格的Web服务进行详细介绍。通过本节内容,不仅可以了解什么是 REST,更能清晰地了解在编写 REST 服务时需要遵守的各个守则,识别真假REST,设计REST API时需要考虑的各种因素,以及实现过程中可能遇到的问题等。
REST(REpresentation State Transfer,表述性状态转移)描述了一个架构样式的网络系统,比如Web应用程序。Roy Fielding还是HTTP规范的主要编写者之一,也是Apache HTTP服务器项目的共同创立者。所以这篇文章一发表,就引起了极大的反响。很多公司或组织如雨后春笋般宣称自己的应用或服务实现了REST API。但该论文实际上只是描述了一种架构风格,并未对具体的实现进行规范。所以各大厂商不免存在误用或滥用REST。所以在这种背景下,Roy Fielding不得不再次发文做了澄清(见REST APIs must be hypertext-driven ),坦言了他的失望,并对SocialSite REST API 提出了批评。同时他指出,除非应用状态引擎是超文本驱动的,否则它就不是REST或REST API。据此,他给出了REST API应该具备的条件:
REST API不应该依赖于任何通信协议,尽管要成功映射到某个协议可能会依赖于元数据的可用性、所选的方法等。
REST API不应该包含对通信协议的任何改动,除非是补充或确定标准协议中未规定的部分。
REST API应该将大部分的描述工作放在定义用于表示资源和驱动应用状态的媒体类型上,或定义现有标准媒体类型的扩展关系名和(或)支持超文本的标记。
REST API绝不应该定义一个固定的资源名或层次结构(客户端和服务器之间的明显耦合)。
REST API永远不应该有那些会影响客户端的“类型化”资源。
REST API不应该要求有先验知识(prior knowledge),除了初始URI和适合目标用户的一组标准化的媒体类型(即它能被任何潜在使用该API的客户端理解)。
REST并非标准,而是一种开发Web应用的架构风格,可以将其理解为一种设计模式。REST基于HTTP、URI及XML这些现有的广泛流行的协议和标准,伴随着REST的应用,HTTP协议得到了更加正确的使用。
REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是REST。
相较于基于SOAP和WSDL的Web服务,REST模式提供了更简单的实现方案。REST Web服务(RESTful Web Services)是松耦合的,特别适用于为客户创建在互联网传播的轻量级的Web服务API。REST应用是以“资源表述的转移(the transfer of representations of resources)”为中心进行请求和响应的。数据和功能均被视为资源,并使用统一的资源标识符(URI)访问资源。网页里面的链接就是典型的 URI。该资源由文档表述,并通过使用一组简单的、定义明确的操作来执行。
例如,一个 REST 资源可能是一个城市当前的天气情况。该资源的表述可能是一个 XML文档、图像文件或HTML页面。客户端可以检索特定表述,通过更新其数据来修改资源,或者完全删除该资源。
目前,越来越多的Web服务开始采用REST风格设计和实现,真实世界中比较著名的REST服务包括Google AJAX搜索API、Amazon Simple Storage Service(Amazon S3)等。
基于REST的Web服务遵循一些基本的设计原则,使得RESTful应用更加简单、轻量,开发速度也更快:
通过URI标识资源 ——系统中的每一个对象或资源都可以通过唯一的URI进行寻址,URI的结构应该简单、可预测且易于理解,比如定义目录结构式的URI。
统一接口 ——以遵循RFC-2616所定义的协议的方式显式地使用HTTP方法,建立创建、检索、更新和删除(CRUD:Create、Retrieve、Update和Delete)操作与HTTP方法之间的一对一映射。
若要在服务器上创建资源,则应该使用POST方法;
若要检索某个资源,则应该使用GET方法;
若要更新或添加资源,则应该使用PUT方法;
若要删除某个资源,则应该使用DELETE方法。
资源多重表述 ——URI所访问的每个资源都可以使用不同的形式加以表示(比如 XML或 JSON),具体的表现形式取决于访问资源的客户端,客户端与服务提供者使用一种内容协商的机制(请求头与MIME类型)来选择合适的数据格式,最小化彼此之间的数据耦合。在REST的世界中,资源即状态,而互联网就是一个巨大的状态机,每个网页是其一个状态;URI是状态的表述;REST风格的应用则是从一个状态迁移到下一个状态的状态转移过程。早期的互联网只有静态页面的时候,通过超链接在静态网页间浏览跳转的 page→link→page→link…模式就是一种典型的状态转移过程。也就是说,早期的互联网就是天然的REST。
无状态 ——对服务器端的请求应该是无状态的,完整、独立的请求不要求服务器在处理请求时检索任何类型的应用程序上下文或状态。无状态约束使服务器的变化对客户端是不可见的,因为在两次连续的请求中,客户端并不依赖于同一台服务器。一个客户端从某台服务器上收到一份包含链接的文档,当它要做一些处理时,这台服务器宕机了,可能是硬盘坏掉被拿去修理,也可能是软件需要升级重启——如果这个客户端访问了从这台服务器接收的链接,则它不会察觉到后台的服务器已经改变了。通过超链接实现有状态交互,即请求消息是自包含的(每次交互都包含完整的信息),有多种技术实现了不同请求间状态信息的传输,例如,URI、cookies和隐藏表单字段等,状态可以嵌入应答消息里,这样一来状态在接下来的交互中仍然有效。REST风格应用可以实现交互,但它却天然地具有服务器无状态的特征。在状态迁移的过程中,服务器不需要记录任何Session,所有的状态都通过 URI 的形式记录在客户端。更准确地说,这里的无状态服务器是指服务器不保存会话状态(Session);而资源本身则是天然的状态,通常是需要被保存的;这里的无状态服务器均指无会话状态服务器。
表2-2是一个HTTP请求方法在RESTful Web服务中的典型应用。
表2-2 HTTP请求方法在RESTful Web服务中的典型应用
针对REST在Java中的规范,主要是JAX-RS(Java API for RESTful Web Services),该规范使得Java程序员可以使用一套固定的接口来开发REST应用,避免依赖于第三方框架。同时,JAX-RS使用POJO编程模型和基于标注的配置,并集成了JAXB,从而可以有效缩短REST应用的开发周期。Java EE 6引入了对JSR-311的支持,Java EE 7支持JSR-339规范。
JAX-RS定义的API位于javax.ws.rs包中。
伴随着JSR 311规范的发布,Sun同步发布了该规范的参考实现Jersey。JAX-RS的具体实现第三方还包括Apache的CXF及JBoss的RESTEasy等。未实现该规范的其他REST框架还包括Spring MVC等。
在Java中,既然规范的制定者和实现者都是Sun公司(现在是Oracle),那么毫无疑问,Jersey就是事实上的标准,对于Java REST的初学者来说要尽量跟着标准走。当然,所有规范的实现在用法上基本没有差别,只是相对来说Jersey的实现更全面一些。
本节所有的例子都是基于Jersey的。若读者对Jersey的参考和实现感兴趣,可参阅笔者另外两本开源电子书《Jersey 2.x用户指南》(https://github.com/waylau/Jersey-2.x-User-Guide)和《REST实战》(https://github.com/waylau/rest-in-action)。
环境准备
JDK 7+;
Maven 3.2.x。
这就是所有必需的环境。当然,也可以根据自己的喜好选择使用IDE。本书使用Eclipse 4.4。
第一个应用
在工作目录下创建第一个Maven管理的应用,执行:
将项目打包成WAR,执行:
mvn clean package
项目打包成功后(见图2-10),可以将打包的WAR(位于./target/simple-service-webapp.war)部署到任意的Servlet容器,比如Tomcat、Jetty、JBoss等。
图2-10 将Maven项目打包成WAR
在浏览器中访问该项目,如图2-11所示。
图2-11 在浏览器中访问该项目
单击“Jersey resource”,可以在页面输出资源“Got it!”字符。
注意: 部署 Jersey 项目,Servlet 容器版本应该不低于2.5,如果想支持更高的特性(比如JAX-RS 2.0 Async Support),则Servlet容器版本应该不低于3.0。
第一个REST项目完成。
探索新项目
simple-service-webapp是一个由Jersey提供的,Maven archetype插件创建的Web项目,在你的项目里随意调整 pom.xml 内的 groupId、包号和版本号就可以创建一个新的项目。此时,simple-service-webapp已经创建,符合Maven的项目结构:
标准的管理配置文件pom.xml;
源文件路径src/main/java;
资源文件路径src/main/resources;
Web应用文件src/main/webapp。
该项目包含一个名为MyResouce的JAX-RS资源类。在src/main/webapp/WEB-INF下,它包含标准的JavaEE Web应用的web.xml部署描述符。项目中的最后一个组件是一个index.jsp页面,其作为这次MyResource资源类打包和部署的应用程序客户端。
MyResource类是JAX-RS的一个实现,源代码如下:
JAX-RS资源是一个可以处理绑定了资源的URI的HTTP请求,且带有注解的POJO。在这个例子中,单一的资源暴露了一个公开的方法,能够处理HTTP GET请求,绑定在/myresource URI 路径下,可以产生媒体类型为“text/plain”的响应消息。在这个示例中,资源返回相同的“Got it!”应对所有客户端的要求。
简单的CURD应用
下面,我们要尝试几个管理系统中常用的CURD操作来模拟一个“用户管理”。
服务端
在服务端,我们要提供REST风格的API。
先创建一个用户对象UserBean.java:
新建一个资源类UserResource.java。添加@Path("users")注解来说明资源根路径是users。添加:
用来在内存中存储数据。可以在userMap中获取我们想要查询的数据。
完整的代码如下:
简单起见,我们约定POST用作新增,PUT用作修改,DELETE用作删除,GET用作查询。
服务端接口开发完毕。
客户端
为了快速测试接口,可以用第三方REST客户端测试程序,这里用的是RESTClient插件,可以在火狐中安装使用。
我们先增加一个用户对象,使用POST请求发送一个JSON格式的数据:
提示报错:415未支持媒体格式的错误,如图2-12所示。
图2-12 未支持媒体格式的错误
由于我们在新增的接口里面设置的是@Consumes(MediaType.APPLICATION_JSON),规定只接收JSON格式,而默认的“Content-Type”是“text/html”,所以还需要在Header里将其设置为“application/json”,如图2-13所示。
图2-13 设置接收的媒体类型
我们再添加一个用户对象:
在响应的数据里面就能看到添加的用户了,如图2-14所示。
图2-14 POST响应
修改用户1的数据:
用PUT请求,如图2-15所示。
在返回的数据里面可以看到用户1被修改了。
现在模拟查询用户的操作,在根据ID查询的接口里面添加如下代码:
图2-15 PUT请求
@Path("{id}")指id这个子路径是一个变量。在查询用户1时,要将用户1的userId放在请求的URI里面(http://localhost:8080/webapi/users/1),如图2-16所示。
图2-16 GET请求
现在模拟删除用户的操作。与上面类似,也用到了@Path("{id}"),如图2-17所示。
图2-17 DELETE请求
用户1被删除了。至此,整个应用完成了。这个“用户管理”够简单吧。
下面介绍几种简洁的REST API设计的最佳实践,可以作为真假REST的一个判别依据。
使用名词来定义接口:
/resources
/resources/1024
不应该使用动词:
/getAllResources
/createNewResource
/deleteAllResources
如果要改变资源的状态,则要使用PUT、POST和DELETE。下面使用GET方法修改user的状态是错误的:
GET /users/711?activate
或
GET /users/711/activate
不要混淆名词的单复数。保持简单,只用复数名词来定义所有资源。
/cars 代替 /car
/users 代替 /user
/products 代替 /product
/settings 代替 /setting
GET /cars/711/drivers/ 返回 711 号 car 的所有 driver 列表
GET /cars/711/drivers/4 返回 711 号 car 的 4 号 driver
客户端、服务端都需要知道相互之间的通信格式。这些格式可以定义在HTTP header里面:
Content-Type定义请求格式;
Accept定义接收相应的格式列表。
HATEOAS(hypermedia as the engine of application state)是REST架构风格中最复杂的约束,也是构建成熟REST 服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约,使客户端可以更加智能和自适应,而 REST 服务本身的演化和更新也变得更加容易。在介绍HATEOAS之前,先介绍一下Richardson提出的REST成熟度模型。该模型把REST服务按照成熟度划分为4个层次:
第一个层次(Level 0)的Web服务使用HTTP作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP和XML-RPC都属于此类。
第二个层次(Level 1)的Web服务引入了资源的概念,每个资源有对应的标识符。
第三个层次(Level 2)的Web服务使用不同的HTTP方法进行不同的操作,并且使用HTTP状态码表示不同的结果。例如,HTTP GET方法用来获取资源,HTTP DELETE方法用来删除资源。
第四个层次(Level 3)的Web服务使用HATEOAS。在资源的表达中包含链接信息,客户端根据链接发现可以执行的动作。
从上述REST成熟度模型中可以看到,使用HATEOAS的REST服务是成熟度最高的,也是推荐的做法。对于不使用HATEOAS的REST服务,客户端和服务器的实现之间是紧密耦合的。客户端需要根据服务器提供的相关文档来了解所暴露的资源和对应的操作。当服务器发生变化,如修改了资源的URI,客户端也需要进行相应的修改。而使用HATEOAS的REST服务中,客户端可以通过服务器提供的资源的表达来智能地发现可以执行的操作。当服务器发生变化,客户端并不需要做出修改,因为资源的URI和其他信息都是动态发现的。
下面是一个HATEOAS的例子:
版本号使用简单的序号,并避免使用点符号,如2.5等。正确用法如下:
/blog/api/v1
HTTP状态码(HTTP Status Code)是用来表示网页服务器HTTP响应状态的3位数字代码。它由RFC 2616规范定义,并得到RFC 2518、RFC 2817、RFC 2295、RFC 2774、RFC 4918等规范的扩展。
在设计 API 处理错误时,应该充分使用 HTTP 状态码,而不是简单地抛出一个“500 –Internal Server Error(内部服务器错误)”。
所有的异常都应该有一个错误的payload作为映射,下面是一个例子: