RESTful-Java-模式和最佳实践-全-

RESTful Java 模式和最佳实践(全)

原文:zh.annas-archive.org/md5/829D0A6DE6895E44AC3D7583B5540457

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

社交网络、云计算和移动应用程序时代的融合,创造了一代新兴技术,使不同的网络设备能够通过互联网相互通信。过去,构建解决方案有传统和专有的方法,涵盖了不同的设备和组件在不可靠的网络或通过互联网相互通信。一些方法,如 RPC CORBA 和基于 SOAP 的 Web 服务,作为面向服务的体系结构(SOA)的不同实现而演变,需要组件之间更紧密的耦合以及更大的集成复杂性。

随着技术格局的演变,今天的应用程序建立在生产和消费 API 的概念上,而不是使用调用服务并生成网页的 Web 框架。这种基于 API 的架构实现了敏捷开发、更容易的采用和普及,以及与企业内外应用程序的规模和集成。

REST 和 JSON 的广泛采用打开了应用程序吸收和利用其他应用程序功能的可能性。REST 的流行主要是因为它能够构建轻量级、简单和成本效益的模块化接口,可以被各种客户端使用。

移动应用程序的出现要求更严格的客户端-服务器模型。在 iOS 和 Android 平台上构建应用程序的公司可以使用基于 REST 的 API,并通过结合来自多个平台的数据来扩展和加深其影响,因为 REST 基于 API 的架构。

REST 具有无状态的额外好处,有助于扩展性、可见性和可靠性,同时也是平台和语言无关的。许多公司正在采用 OAuth 2.0 进行安全和令牌管理。

本书旨在为热心读者提供 REST 架构风格的概述,重点介绍所有提到的主题,然后深入探讨构建轻量级、可扩展、可靠和高可用的 RESTful 服务的最佳实践和常用模式。

本书涵盖的内容

《第一章》REST - 起源,从 REST 的基本概念开始,介绍了如何设计 RESTful 服务以及围绕设计 REST 资源的最佳实践。它涵盖了 JAX-RS 2.0 API 在 Java 中构建 RESTful 服务。

《第二章》资源设计,讨论了不同的请求响应模式;涵盖了内容协商、资源版本控制以及 REST 中的响应代码等主题。

《第三章》安全和可追溯性,涵盖了关于 REST API 的安全和可追溯性的高级细节。其中包括访问控制、OAuth 身份验证、异常处理以及审计和验证模式等主题。

《第四章》性能设计,涵盖了性能所需的设计原则。它讨论了 REST 中的缓存原则、异步和长时间运行的作业,以及如何使用部分更新。

《第五章》高级设计原则,涵盖了高级主题,如速率限制、响应分页以及国际化和本地化原则,并提供了详细的示例。它涵盖了可扩展性、HATEOAS 以及测试和文档化 REST 服务等主题。

第六章新兴标准和 REST 的未来,涵盖了使用 WebHooks、WebSockets、PuSH 和服务器发送事件服务的实时 API,并在各个领域进行了比较和对比。此外,本章还涵盖了案例研究,展示了新兴技术如 WebSockets 和 WebHooks 在实时应用中的使用。它还概述了 REST 在微服务中的作用。

附录涵盖了来自 GitHub、Twitter 和 Facebook 的不同 REST API,以及它们如何与第二章资源设计中讨论的原则联系起来,一直到第五章高级设计原则

您需要什么来阅读这本书

为了能够构建和运行本书提供的示例,您需要以下内容:

这本书是为谁准备的

这本书是应用程序开发人员熟悉 REST 的完美阅读来源。它深入探讨了细节、最佳实践和常用的 REST 模式,以及 Facebook、Twitter、PayPal、GitHub、Stripe 和其他公司如何使用 RESTful 服务实现解决方案的见解。

约定

在这本书中,您会发现许多不同类型信息的文本样式。以下是一些这些样式的例子,以及它们的含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"GETHEAD是安全方法。"

代码块设置如下:

    @GET
    @Path("orders")
    public List<Coffee> getOrders() {
        return coffeeService.getOrders();    }

当我们希望引起您对代码块的特定部分的注意时,相关行或项目以粗体设置:

@Path("v1/coffees")
public class CoffeesResource {
    @GET
    @Path("orders")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Coffee> getCoffeeList( ){
      //Implementation goes here

    }

任何命令行输入或输出都是这样写的:

#  curl -X GET http://api.test.com/baristashop/v1.1/coffees

新术语重要单词以粗体显示。

注意

警告或重要说明出现在这样的框中。

提示

提示和技巧看起来像这样。

第一章:REST - 从哪里开始

传统 SOA 格式的 Web 服务已经存在很长时间,用于实现应用程序之间的异构通信。支持这种通信的一种方式是使用简单对象访问协议SOAP)/Web 服务描述语言WSDL)方法。SOAP/WSDL 是一种基于 XML 的标准,在服务之间存在严格的合同时运行良好。我们现在处于分布式服务的时代,Web、移动客户端以及其他服务(内部或外部)可以利用不同供应商和开源平台提供的 API。这种要求强调了分布式服务之间信息交换的需求,以及可预测、健壮、明确定义的接口。

HTTP 1.1 在 RFC 2616 中定义,并且被广泛用作分布式协作超媒体信息系统的标准协议。表述状态转移REST)受到 HTTP 的启发,可以在任何使用 HTTP 的地方使用。本章将介绍 RESTful 服务设计的基础知识,并展示如何基于标准 Java API 生成和消费 RESTful 服务。

本章涵盖以下主题。

  • REST 介绍

  • 安全性和幂等性

  • 构建 RESTful 服务的设计原则

  • RESTful 服务的 Java 标准 API

  • 设计 RESTful 服务的最佳实践

REST 介绍

REST 是一种符合 Web 标准的架构风格,例如使用 HTTP 动词和 URI。它受以下原则约束:

  • 所有资源都由 URI 标识

  • 所有资源都可以有多种表示

  • 所有资源都可以通过标准 HTTP 方法进行访问/修改/创建/删除

  • 服务器上没有状态信息

REST 和无状态性

REST 受无状态性原则约束。客户端到服务器的每个请求必须具有理解请求的所有细节。这有助于提高请求的可见性、可靠性和可扩展性。

可见性得到改善,因为监视请求的系统不必查看超出一个请求以获取详细信息。可靠性得到改善,因为在部分故障的情况下不需要检查点/恢复。可扩展性得到改善,因为服务器可以处理的请求数量增加,因为服务器不负责存储任何状态。

Roy Fielding 关于 REST 架构风格的论文详细介绍了 REST 的无状态性。请访问www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm获取更多信息。

通过对 REST 的基础知识进行初步介绍,我们将在下一节中介绍不同的成熟度级别以及 REST 在其中的位置。

Richardson 成熟度模型

Richardson 成熟度模型是由 Leonard Richardson 开发的模型。它从资源、动词和超媒体控制的角度讨论了 REST 的基础知识。成熟度模型的起点是使用 HTTP 层作为传输。如下图所示:

Richardson 成熟度模型

0 级 - 远程过程调用

0 级包含将数据作为普通旧 XMLPOX)发送的 SOAP 或 XML-RPC。只使用POST方法。这是构建具有单个POST方法的 SOA 应用程序的最原始方式,并使用 XML 在服务之间进行通信。

1 级 - REST 资源

1 级使用POST方法,而不是使用函数和传递参数,而是使用 REST URI。因此,它仍然只使用一个 HTTP 方法。它比 0 级更好,因为它将复杂功能分解为多个资源,并使用一个POST方法在服务之间进行通信。

2 级 - 更多的 HTTP 动词

Level 2 使用其他 HTTP 动词,如GETHEADDELETEPUT,以及POST方法。 Level 2 是 REST 的真正用例,它倡导根据 HTTP 请求方法使用不同的动词,系统可以具有多个资源。

Level 3 – HATEOAS

超媒体作为应用状态的引擎HATEOAS)是 Richardson 模型的最成熟级别。对客户端请求的响应包含超媒体控件,这可以帮助客户端决定下一步可以采取什么行动。 Level 3 鼓励易于发现,并使响应易于自我解释。关于 HATEOAS 是否真正符合 RESTful 存在争议,因为表示包含了除了描述资源之外的更多信息。我们将展示一些平台如 PayPal 如何在其 API 的一部分中实现 HATEOAS 的详细信息在第五章,“高级设计原则”中。

下一节涵盖了安全性和幂等性,这是处理 RESTful 服务时的两个重要术语。

安全性和幂等性

下一节将详细讨论什么是安全和幂等方法。

安全方法

安全方法是不会改变服务器状态的方法。例如,GET /v1/coffees/orders/1234是一个安全方法。

注意

安全方法可以被缓存。GETHEAD是安全方法。

PUT方法不安全,因为它会在服务器上创建或修改资源。POST方法由于相同的原因也不安全。DELETE方法不安全,因为它会删除服务器上的资源。

幂等方法

幂等方法是一种无论调用多少次都会产生相同结果的方法。

注意

GET方法是幂等的,因为对GET资源的多次调用将始终返回相同的响应。

PUT方法是幂等的,多次调用PUT方法将更新相同的资源并且不会改变结果。

POST不是幂等的,多次调用POST方法可能会产生不同的结果,并且会导致创建新资源。DELETE是幂等的,因为一旦资源被删除,它就消失了,多次调用该方法不会改变结果。

构建 RESTful 服务的设计原则

以下是设计、开发和测试 RESTful 服务的过程。我们将在本章中详细介绍每个过程:

  • 识别资源 URI

此过程涉及决定名词将代表您的资源。

  • 识别资源支持的方法

此过程涉及使用各种 HTTP 方法进行 CRUD 操作。

  • 识别资源支持的不同表示

此步骤涉及选择资源表示应该是 JSON、XML、HTML 还是纯文本。

  • 使用 JAX-RS API 实现 RESTful 服务

API 需要基于 JAX-RS 规范实现

  • 部署 RESTful 服务

将服务部署在诸如 Tomcat、Glassfish 和 WildFly 之类的应用容器上。示例展示了如何创建 WAR 文件并在 Glassfish 4.0 上部署,它可以与任何符合 JavaEE 7 标准的容器一起使用。

  • 测试 RESTful 服务

编写客户端 API 以测试服务,或使用 curl 或基于浏览器的工具来测试 REST 请求。

识别资源 URI

RESTful 资源由资源 URI 标识。由于使用 URI 来标识资源,REST 是可扩展的。

以下表格显示了示例 URI,可以表示系统中的不同资源:

URI URI 的描述
/v1/library/books 用于表示图书馆中的一组图书资源
/v1/library/books/isbn/12345678 用于表示由其 ISBN“12345678”标识的单本书
/v1/coffees 用于表示咖啡店出售的所有咖啡
/v1/coffees/orders 这用于表示所有已订购的咖啡
/v1/coffees/orders/123 这用于表示由“123”标识的咖啡订单
/v1/users/1235 这用于表示系统中由“1235”标识的用户
/v1/users/5034/books 这用于表示由“5034”标识的用户的所有书籍

所有前面的示例都显示了一个清晰可读的模式,客户端可以解释。所有这些资源都可以有多个表示。在前面的表中显示的这些资源示例可以由 JSON、XML、HTML 或纯文本表示,并且可以通过 HTTP 方法GETPUTPOSTDELETE进行操作。

识别资源支持的方法

HTTP 动词占据了统一接口约束的主要部分,该约束定义了动词识别的操作与基于名词的 REST 资源之间的关联。

以下表格总结了 HTTP 方法和对资源采取的操作的描述,以图书馆中书籍集合的简单示例为例。

HTTP 方法 资源 URI 描述
GET /library/books 这获取书籍列表
GET /library/books/isbn/12345678 这获取由 ISBN“12345678”标识的书籍
POST /library/books 这创建一个新的书籍订单
DELETE /library/books/isbn/12345678 这将删除由 ISBN“12345678”标识的书籍
PUT /library/books/isbn/12345678 这将更新由 ISBN“12345678”标识的特定书籍
PATCH /library/books/isbn/12345678 这可用于对由 ISBN“12345678”标识的书籍进行部分更新

下一节将介绍每个 HTTP 动词在 REST 上下文中的语义。

HTTP 动词和 REST

HTTP 动词告诉服务器如何处理作为 URL 一部分发送的数据。

获取

GET方法是 HTTP 的最简单动词,它使我们能够访问资源。每当客户端在浏览器中点击 URL 时,它会向 URL 指定的地址发送GET请求。GET是安全和幂等的。GET请求被缓存。GET请求中可以使用查询参数。

例如,检索所有活动用户的简单GET请求如下所示:

curl http://api.foo.com/v1/users/12345?active=true

POST

POST用于创建资源。POST请求既不是幂等的,也不是安全的。多次调用POST请求可以创建多个资源。

如果存在缓存条目,POST请求应该使缓存条目无效。不鼓励在POST请求中使用查询参数。

例如,创建用户的POST请求可以如下所示:

curl –X POST  -d'{"name":"John Doe","username":"jdoe", "phone":"412-344-5644"}' http://api.foo.com/v1/users

放置

PUT用于更新资源。PUT是幂等的,但不安全。多次调用PUT请求应该通过更新资源产生相同的结果。

如果存在缓存条目,PUT请求应该使缓存条目无效。

例如,更新用户的PUT请求可以如下所示:

curl –X PUT  -d'{ "phone":"413-344-5644"}'
http://api.foo.com/v1/users

DELETE

DELETE用于删除资源。DELETE是幂等的,但不安全。这是幂等的,因为根据 RFC 2616,N > 0 请求的副作用与单个请求相同。这意味着一旦资源被删除,多次调用DELETE将获得相同的响应。

例如,删除用户的请求可以如下所示:

curl –X DELETE http://foo.api.com/v1/users/1234

HEAD类似于GET请求。不同之处在于只返回 HTTP 标头,不返回内容。HEAD是幂等和安全的。

例如,使用 curl 发送HEAD请求的请求如下所示:

curl –X HEAD http://foo.api.com/v1/users

提示

在尝试使用GET请求获取大型表示之前,发送HEAD请求以查看资源是否已更改可能很有用。

PUT 与 POST

根据 RFC,PUTPOST之间的区别在于请求 URI。由POST标识的 URI 定义将处理POST请求的实体。PUT请求中的 URI 包括请求中的实体。

因此,POST /v1/coffees/orders表示创建一个新资源并返回一个标识符来描述该资源。相反,PUT /v1/coffees/orders/1234表示更新由"1234"标识的资源(如果存在);否则创建一个新订单并使用orders/1234 URI 来标识它。

注意

PUTPOST都可以用于创建或更新方法。方法的使用取决于期望从方法获得的幂等行为以及用于标识资源的位置。

下一节将介绍如何识别资源的不同表示形式。

识别资源的不同表示形式

RESTful 资源是抽象实体,需要在与客户端通信之前被序列化为表示。资源的常见表示可以是 XML、JSON、HTML 或纯文本。资源可以根据客户端的处理能力向客户端提供表示。客户端可以指定它偏好的语言和媒体类型。这被称为内容协商。第二章,“资源设计”,详细介绍了内容协商主题。

实现 API

现在我们对设计 RESTful 资源和将 HTTP 动词与资源上的操作关联有了一些了解,我们将介绍实现 API 和构建 RESTful 服务所需的内容。本节将涵盖以下主题:

  • 用于 RESTful 服务的 Java API(JAX-RS)

用于 RESTful 服务的 Java API(JAX-RS)

用于 RESTful 服务的 Java API 提供了用于构建和开发基于 REST 架构风格的应用程序的可移植 API。使用 JAX-RS,Java POJO 可以作为 RESTful web 资源公开,这些资源独立于底层技术,并使用基于注释的简单 API。

JAX-RS 2.0 是规范的最新版本,与其前身 JAX-RS 1.0 相比,在以下领域特别是具有更新的功能:

  • Bean 验证支持

  • 客户端 API 支持

  • 异步调用支持

Jersey 是 JAX-RS 规范的实现。

我们将在随后的章节中详细介绍所有这些主题。我们正在演示一个简单的咖啡店示例,您可以在其中创建一个名为CoffeesResource的 REST 资源,该资源可以执行以下操作:

  • 提供已下订单的详细信息

  • 创建新订单

  • 获取特定订单的详细信息

要创建一个 RESTful 资源,我们从一个名为CoffeesResource的 POJO 开始。以下是 JAX-RS 资源的示例:

@Path("v1/coffees")
public class CoffeesResource {

    @GET
    @Path("orders")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Coffee> getCoffeeList( ){
      //Implementation goes here

    }
  1. 如前面的代码所示,我们创建了一个名为CoffeesResource的小型 POJO。我们使用@Path("v1/coffees")对类进行注释,该注释标识了该类为请求提供服务的 URI 路径。

  2. 接下来,我们定义了一个名为getCoffeeList()的方法。该方法具有以下注释:

  • @GET:这表示被注释的方法代表一个 HTTP GET请求。

  • @PATH:在此示例中,GET请求v1/coffees/orders将由getCoffeeList()方法处理。

  • @Produces:这定义了此资源生成的媒体类型。在我们之前的片段中,我们定义了MediaType.APPLICATION_JSON,其值为application/json

  1. 另一种创建订单的方法如下:
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @ValidateOnExecution
    public Response addCoffee(@Valid Coffee coffee) {
    //Implementation goes here
    }

对于创建订单的第二种方法,我们定义了一个名为addCoffee()的方法。该方法具有以下注释:

  • @POST:这表示被注释的方法代表 HTTP POST请求。

  • @Consumes:这定义了此资源消耗的媒体类型。在我们之前的片段中,我们定义了MediaType.APPLICATION_JSON,其值为application/json

  • @Produces:这定义了此资源生成的媒体类型。在我们之前的片段中,我们定义了MediaType.APPLICATION_JSON,其值为application/json

  • @ValidateOnExecution:这指定了应在执行时验证其参数或返回值的方法。有关@ValidateOnExecution@Valid注释的更多详细信息将在第三章安全性和可追溯性中介绍。

因此,我们看到了一个简单示例,说明了将简单的 POJO 转换为 REST 资源有多么容易。现在,我们将介绍Application子类,该子类将定义 JAX-RS 应用程序的组件,包括元数据。

以下是名为CoffeeApplication的示例Application子类的代码:

@ApplicationPath("/")
public class CoffeeApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<Class<?>>();
        classes.add(CoffeesResource.class);
        return classes;
    }

如前面的代码片段所示,getClasses()方法已被重写,并且我们将CoffeesResource类添加到Application子类中。Application类可以是 WAR 文件中的WEB-INF/classesWEB-INF/lib的一部分。

部署 RESTful 服务

一旦我们创建了资源并将元信息添加到 Application 子类中,下一步就是构建 WAR 文件。WAR 文件可以部署在任何 servlet 容器上。

示例的源代码作为本书的可下载捆绑包的一部分提供,其中将详细介绍部署和运行示例的步骤。

测试 RESTful 服务

然后,我们可以使用 JAX-RS 2.0 提供的 Client API 功能来访问资源。

本节将涵盖以下主题:

  • JAX-RS 2.0 的 Client API

  • 使用 curl 或名为 Postman 的基于浏览器的扩展访问 RESTful 资源

JAX-RS 2.0 的 Client API

JAX-RS 2.0 为访问 RESTful 资源提供了更新的 Client API。客户端 API 的入口点是javax.ws.rs.client.Client

使用 JAX-RS 2.0 中新引入的 Client API,可以访问端点如下:

Client client = ClientFactory.newClient();
WebTarget target = client.target("http://. . ./coffees/orders");
String response = target.request().get(String.class);

如前面的代码片段所示,使用ClientFactory.newClient()方法获取了客户端的默认实例。使用target方法,我们创建了一个WebTarget对象。然后使用这些目标对象通过添加方法和查询参数来准备请求。

在这些 API 之前,我们访问 REST 资源的方式是这样的:

URL url = new URL("http://. . ./coffees/orders");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.setDoOutput(false);
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
    //. . .
}

因此,我们可以看到 JAX-RS 2.0 客户端 API 支持已经改进,以避免使用HTTPURLConnection,而是使用流畅的客户端 API。

如果请求是POST请求:

Client client = ClientBuilder.newClient();
Coffee coffee = new Coffee(...);
WebTarget myResource = client.target("http://foo.com/v1/coffees");
myResource.request(MediaType.APPLICATION_XML) .post(Entity.xml(coffee), Coffee.class);

WebTarget.request()方法返回一个javax.ws.rs.client.InvocationBuilder,它使用post()方法来调用 HTTP POST请求。post()方法使用Coffee实例的实体,并指定媒体类型为"APPLICATION_XML"

MessageBodyReaderWriter实现已在客户端中注册。有关MessageBodyReaderMessageBodyWriter的更多信息将在第二章资源设计中介绍。

以下表格总结了到目前为止我们所涵盖的一些主要 JAX-RS 类/注释。

注释名称 描述
javax.ws.rs.Path 这标识了资源为方法提供的 URI 路径
javax.ws.rs.ApplicationPath 这被Application的子类用作应用程序中所有资源提供的所有 URI 的基本 URI
javax.ws.rs.Produces 这定义了资源可以生成的媒体类型
javax.ws.rs.Consumes 这定义了资源可以消耗的媒体类型
javax.ws.rs.client.Client 这定义了客户端请求的入口点
javax.ws.rs.client.WebTarget 这定义了由 URI 标识的资源目标

注意

客户端是帮助简化客户端通信基础设施的重量级对象。因此,建议在应用程序中仅构建少量客户端实例,因为初始化和处理客户端实例可能是一个相当昂贵的操作。此外,必须在处理之前正确关闭客户端实例,以避免资源泄漏。

访问 RESTful 资源

以下部分涵盖了客户端可以访问和测试 REST 资源的不同方式。

cURL

cURL 是一个用于测试 REST API 的流行命令行工具。cURL 库和 cURL 命令使用户能够创建请求,将其放在管道上,并探索响应。以下是一些用于一些基本功能的curl请求的示例:

curl 请求 描述
curl http://api.foo.com/v1/coffees/1 这是一个简单的GET请求
curl -H "foo:bar" http://api.foo.com/v1/coffees 这是一个使用-H添加请求头的curl请求的示例
curl -i http://api.foo.com/v1/coffees/1 这是一个使用-i查看响应头的curl命令的示例
curl –X POST -d'{"name":"John Doe","username":"jdoe", "phone":"412-344-5644"} http://api.foo.com/v1/users 这是一个用于创建用户的POST方法的curl请求的示例

尽管 cURL 非常强大,但有很多选项需要记住和使用。有时,使用基于浏览器的工具来开发 REST API,如 Postman 或高级 REST 客户端,会有所帮助。

Postman

Chrome 浏览器上的 Postman 是一个非常好的测试和开发 REST API 的工具。它具有用于呈现数据的 JSON 和 XML 查看器。它还可以允许预览 HTTP 1.1 请求,重播,并组织请求以供将来使用。Postman 与浏览器共享相同的环境,也可以显示浏览器 cookie。

Postman 相对于 cURL 的优势在于有一个很好的用户界面,可以输入参数,用户不需要处理命令或脚本。还支持各种授权方案,如基本用户认证和摘要访问认证。

以下是一张截图,显示了如何在 Postman 中发送查询:

Postman

如前面的截图所示,我们看到了 Postman 应用程序。测试 Postman 的一个简单方法是从 Chrome 启动 Postman 应用程序。

然后,选择 HTTP 方法GET并粘贴api.postcodes.io/random/postcodes URL。(PostCodes 是一个基于地理数据的免费开源服务。)

您将看到一个 JSON 响应,类似于这样:

{
    "status": 200,
    "result": {
        "postcode": "OX1 9SN",
        "quality": 1,
        "eastings": 451316,
        "northings": 206104,
        "country": "England",
        "nhs_ha": "South Central",
        "admin_county": "Oxfordshire",
        "admin_district": "Oxford",
        "admin_ward": "Carfax",
…}
}

提示

下载示例代码

您可以从您在www.packtpub.com购买的 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

在前面截图的左侧窗格中有不同的查询,这些查询已经根据本书中的各种示例添加到了一个集合中,例如获取所有咖啡订单,获取特定订单,创建订单等等。您也可以类似地创建自定义查询集合。

注意

要了解更多详情,请访问www.getpostman.com/

其他工具

以下是一些在处理 REST 资源时非常有用的其他工具。

高级 REST 客户端

高级 REST 客户端是另一个基于 Google WebToolkit 的 Chrome 扩展,允许用户测试和开发 REST API。

JSONLint

JSONLint 是一个简单的在线验证器,可确保 JSON 有效。在发送 JSON 数据作为请求的一部分时,验证数据格式是否符合 JSON 规范是有用的。在这种情况下,客户端可以使用 JSONLint 验证输入。要了解更多详情,请访问jsonlint.com/

设计资源时的最佳实践

以下部分突出显示了设计 RESTful 资源时的一些最佳实践:

  • API 开发者应该使用名词来理解和浏览资源,使用 HTTP 方法和动词,例如,/user/1234/books 比/user/1234/getBook URI 更好。

  • 在 URI 中使用关联来标识子资源。例如,要获取用户 1234 的书籍 5678 的作者,使用以下 URI:/user/1234/books/5678/authors

  • 对于特定的变化,使用查询参数。例如,要获取所有具有 10 条评论的书籍,使用/user/1234/books?reviews_counts=10

  • 如果可能,允许部分响应作为查询参数的一部分。例如,在获取用户的姓名和年龄时,客户端可以指定?fields作为查询参数,并使用/users/1234?fields=name,age URI 指定应该由服务器在响应中发送的字段列表。

  • 在客户端没有指定感兴趣的格式时,为响应的输出格式设置默认值。大多数 API 开发人员选择将 JSON 作为默认响应 MIME 类型发送。

  • 使用 camelCase 或使用_作为属性名称。

  • 支持标准 API 以获取计数,例如users/1234/books/count,以便客户端可以了解响应中可以期望多少对象。

这也将帮助客户端进行分页查询。关于分页的更多细节将在第五章中涵盖,高级设计原则

  • 支持漂亮打印选项,users/1234?pretty_print。另外,不缓存带有漂亮打印查询参数的查询是一个良好的实践。

  • 尽量详细地避免啰嗦。这是因为如果服务器在响应中没有提供足够的细节,客户端需要进行更多的调用以获取额外的细节。这不仅浪费了网络资源,还会影响客户端的速率限制。关于速率限制的更多细节在第五章中有所涵盖,高级设计原则

推荐阅读

以下链接可能对查看更多细节有用:

摘要

在本章中,我们介绍了 REST、CRUD API 的基础知识以及如何设计 RESTful 资源。我们使用了基于 JAX-RS 2.0 的注解来表示 HTTP 方法,以及可以用于定位资源的客户端 API。此外,我们还总结了设计 RESTful 服务时的最佳实践。

下一章将更深入地探讨这里涵盖的概念。我们还将涵盖诸如内容协商、JAX-RS 2.0 中的实体提供者、错误处理、版本控制方案和 REST 中的响应代码等主题。我们将探讨服务器可以使用流式传输或分块传输向客户端发送响应的技术。

第二章:资源设计

第一章,“REST - 起源”,介绍了 REST 的基础知识以及在设计 RESTful 资源时的最佳实践。本章将继续讨论请求响应模式的理解,如何处理资源的不同表示,API 版本控制的不同策略,以及如何使用标准 HTTP 代码来处理 REST 响应。本章的子章节将涵盖以下主题:

  • REST 响应模式

  • 内容协商

  • 实体提供程序和不同的表示

  • API 版本控制

  • 响应代码和 REST 模式

我们还将介绍用于序列化和反序列化请求和响应实体的自定义实体提供程序,以及流式传输和分块等其他方法。

REST 响应模式

在前一章中,我们看到了如何使用与域相关的数据来创建可读的 URI,使用不同的 CRUD 功能的 HTTP 方法,并使用标准化的 MIME 类型和 HTTP 响应代码在客户端和服务器之间传输数据。

以下是显示标准 REST 请求/响应模式的图表:

REST 响应模式

如前图所示,客户端发出 REST 请求,其中包括标准的 HTTP 方法、MIME 类型和目标 URI。服务器处理请求并发送回一个响应,其中包括标准的 HTTP 响应代码和 MIME 类型。我们之前介绍了 HTTP 方法以及如何使用 JAX-RS 注释。还列举了设计资源 URI 的最佳实践。在本章中,我们将介绍常用的 HTTP 响应代码以及如何处理不同的 MIME 类型。

内容协商

内容协商意味着在同一 URI 中允许资源的不同表示,以便客户端可以选择最适合它们的表示。

“HTTP 有几种机制来进行‘内容协商’-在有多个表示可用时选择给定响应的最佳表示的过程。”
--RFC 2616, Fielding et al.

内容协商有不同的模式。具体如下:

  • 使用 HTTP 头

  • 使用 URL 模式

使用 HTTP 头进行内容协商

当客户端发送请求以创建或更新资源时,应从客户端传输某种有效负载到端点。此外,生成响应时,有效负载可以发送回客户端。这些有效负载由 HTTP 请求和响应实体处理,这些实体作为 HTTP 消息正文的一部分发送。

实体通常通过请求发送,通常用于 HTTP POSTPUT 方法,或者在 HTTP 方法的响应中返回。Content-Type HTTP 头用于指示服务器发送的实体的 MIME 类型。常见的内容类型示例包括"text/plain""application/xml""text/html""application/json""image/gif""image/jpeg"

客户端可以向服务器发出请求,并在AcceptHTTP 头的一部分中指定它可以处理的媒体类型以及其首选顺序。客户端还可以在"Accept-Language"头的一部分中指定它希望响应的语言。如果请求中没有Accept头,则服务器可以发送它选择的表示。

JAX-RS 规范提供了标准注释来支持内容协商。这些是javax.ws.rs.Producesjavax.ws.rs.Consumes注释。以下代码段显示了资源方法中@Produces注释的示例:

    @GET
    @Path("orders")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Coffee> getCoffeeList(){
        return CoffeeService.getCoffeeList();

    }

getCoffeeList()方法返回咖啡列表,并用@Produces(MediaType.APPLICATION_JSON)进行注释。@Produces注释用于指定资源可以发送给客户端的 MIME 类型,并将其与客户端的Accept头进行匹配。

此方法将产生如下响应:

X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.0  Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition  4.0 
Content-Type: application/json
Date: Thu, 31 Jul 2014 15:25:17 GMT
Content-Length: 268
{
    "coffees": [
        {
            "Id": 10,
            "Name": "Cappuchino",
            "Price": 3.82,
            "Type": "Iced",
            "Size": "Medium"
        },
        {
            "Id": 11,
            "Name": "Americano",
            "Price": 3.42,
            "Type": "Brewed",
            "Size": "Large"
        }
    ]
}

在资源中,如果没有方法能够生成客户端请求的 MIME 类型,JAX-RS 运行时会返回 HTTP 406 Not Acceptable错误。

以下代码片段显示了一个使用@Consumes注解的资源方法:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response addCoffee(Coffee coffee) {
        // Implementation here
    }

@Consumes注解指定了资源可以消费的媒体类型。当客户端发出请求时,JAX-RS 会找到所有与路径匹配的方法,然后根据客户端发送的内容类型调用方法。

如果资源无法消费客户端请求的 MIME 类型,JAX-RS 运行时会返回 HTTP 415 ("Unsupported Media Type")错误。

可以在@Produces@Consumes注解中指定多个 MIME 类型,如@Produces(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)

除了对静态内容协商的支持,JAX-RS 还包含使用javax.ws.rs.core.Variant类和javax.ws.rs.core.Request对象的运行时内容协商支持。在 JAX-RS 规范中,Variant对象是媒体类型、内容语言和内容编码以及 ETags、最后修改的标头和其他先决条件的组合。Variant对象定义了服务器支持的资源表示。Variant.VariantListBuilder类用于构建表示变体列表。

以下代码片段显示了如何创建资源表示变体列表:

List<Variant>  variants = Variant.mediatypes("application/xml", "application/json").build();

代码片段调用了VariantListBuilder类的build方法。Request.selectVariant方法接受Variant对象列表,并根据客户端的Accept标头选择其中一个,如下面的代码片段所示:

@GET
public Response getCoffee(@Context Request r) { 
    List<Variant> vs = ...;
    Variant v = r.selectVariant(vs);
    if (v == null) {
        return Response.notAcceptable(vs).build();
    } else {
        Coffee coffee = ..//select the representation based on v
        return Response.ok(coffee, v);
    }
}

基于 URL 模式的内容协商

一些 API 采用的内容协商的另一种方法是根据 URL 中资源的扩展名发送资源表示。例如,客户端可以使用http://foo.api.com/v2/library/books.xmlhttp://foo.api.com/v2/library/books.json来请求详细信息。服务器有不同的方法来处理这两个 URI。然而,这两者都是同一资源的表示。

@Path("/v1/books/")
public class BookResource {
    @Path("{resourceID}.xml")
    @GET 
    public Response getBookInXML(@PathParam("resourceID") String resourceID) {
        //Return Response with entity in XML 
             }

    @Path("{resourceID}.json")
    @GET
    public Response getBookInJSON(@PathParam("resourceID") String resourceID) {
        //Return Response with entity in JSON
    }
}

如前面的代码片段所示,定义了两个方法:getBookInXML()getBookInJSON(),响应是根据 URL 路径返回的。

提示

使用 HTTP 内容协商Accept标头是一个很好的做法。使用标头进行内容协商可以清晰地将 IT 关注点与业务分开。使用Accept标头进行内容协商的另一个优势是只有一个资源方法适用于所有不同的表示形式。

以下部分介绍了如何使用 JAX-RS 中的实体提供程序将资源序列化和反序列化为不同的表示形式。

实体提供程序和不同的表示形式

在前面的示例中,我们将从 URI 路径片段和请求的查询参数中提取的文字参数传递给资源方法。然而,有时我们希望在请求主体中传递有效负载,例如POST请求。JAX-RS 提供了两个可用的接口:一个用于处理入站实体表示到 Java 反序列化的javax.ws.rs.ext.MessageBodyReader,另一个用于处理出站实体 Java 到表示序列化的javax.ws.rs.ext.MessageBodyWriter

MessageBodyReader将实体从消息主体表示反序列化为 Java 类。MessageBodyWriter将 Java 类序列化为特定表示格式。

以下表格显示了需要实现的方法:

MessageBodyReader 的方法 描述
isReadable() 用于检查MessageBodyReader类是否支持从流到 Java 类型的转换
readFrom() 用于从InputStream类中读取类型

如表所示,MessageBodyReader实现类的isReadable()方法用于检查MessageBodyReader是否能处理指定的输入。当调用MessageBodyReader类的readFrom()方法时,它可以将输入流转换为 Java POJO。

下表显示了必须实现的MessageBodyWriter方法以及每个方法的简要描述:

MessageBodyWriter 方法 描述
isWritable() 用于检查MessageBodyWriter类是否支持从指定的 Java 类型进行转换
getSize() 用于检查字节的长度,如果大小已知则返回长度,否则返回-1
writeTo() 用于从一种类型写入流

MessageBodyWriter实现类的isWritable()方法用于检查MessageBodyWriter类是否能处理指定的输入。当调用MessageBodyWriterwriteTo()方法时,它可以将 Java POJO 转换为输出流。本书的下载包中的示例展示了如何使用MessageBodyReaderMessageBodyWriter

然而,还有一些轻量级的实现,如StreamingOutputChunkingOutput类,接下来的部分将介绍 JAX-RS 的 Jersey 实现已经支持基本格式,如文本、JSON 和 XML。

StreamingOutput

javax.ws.rs.core.StreamingOutput类是一个回调,可以在应用程序希望流式传输输出时实现以发送响应中的实体。StreamingOutput类是javax.ws.rs.ext.MessageBodyWriter类的轻量级替代品。

以下是一个示例代码,展示了如何在响应的一部分中使用StreamingOutput

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/orders/{id}")
    public Response streamExample(@PathParam("id") int id) {
        final Coffee coffee = CoffeeService.getCoffee(id);
        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream os) throws IOException,
                    WebApplicationException {
                Writer writer = new BufferedWriter(new OutputStreamWriter(os));
                writer.write(coffee.toString());
                writer.flush();
            }
        };
        return Response.ok(stream).build();
    }

如前面的片段所示,StreamingOutput类的write()方法已被重写以写入输出流。StreamingOutput在以流的方式流式传输二进制数据时非常有用。要了解更多详情,请查看作为下载包的一部分提供的示例代码。

ChunkedOutput

使用 JAX-RS 的 Jersey 实现,服务器可以使用org.glassfish.jersey.server.ChunkedOutput类在可用时立即以块的形式向客户端发送响应,而无需等待其他块也变为可用。size对象的值为-1 将在响应的Content-Length头中发送,以指示响应将被分块。在客户端,它将知道响应将被分块,因此它将单独读取每个响应的块并处理它,并等待更多块在同一连接上到来。服务器将继续发送响应块,直到在发送最后一个块后关闭连接并完成响应处理。

以下是一个示例代码,展示了如何使用ChunkedOutput

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/orders/{id}/chunk")
    public ChunkedOutput<String> chunkExample(final @PathParam("id") int id) {
        final ChunkedOutput<String> output = new ChunkedOutput<String>(String.class);

        new Thread() {
            @Override
            public void run() {
                try {
                    output.write("foo");
                    output.write("bar");
                    output.write("test");
                } catch (IOException e) {
                   e.printStackTrace();
                } finally {
                    try {
                        output.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        return output;

    }
}

如片段所示,chunkExample方法返回一个ChunkedOutput对象。

在客户端,org.glassfish.jersey.client.ChunkedInput可用于接收以“类型化”块接收消息。这种数据类型对于从大型或连续数据输入流中消耗部分响应非常有用。以下片段显示了客户端如何从ChunkedInput类中读取:

ChunkedInput<String> input = target().path("..").request().get(new GenericType<ChunkedInput<String>>() {
        });
while ((chunk = chunkedInput.read()) != null) {
    //Do something
}

注意

ChunkedOutput 和 StreamingOutput 之间的区别

ChunkedOutput是 Jersey 提供的内部类。它允许服务器在不关闭客户端连接的情况下发送数据的。它使用一系列方便的调用ChunkedOutput.write方法,该方法接受 POJO 和媒体类型输入,然后使用 JAX-RS 的MessageBodyWriter类将 POJO 转换为字节。ChunkedOutput的写入是非阻塞的。

StreamingOutput是一个低级别的 JAX-RS API,直接使用字节。服务器必须实现StreamingOutput,并且其write(OutputStream)方法将被 JAX-RS 运行时调用一次,并且调用是阻塞的。

Jersey 和 JSON 支持

Jersey 在处理 JSON 表示时提供了以下支持方法。

基于 POJO 的 JSON 绑定支持

基于 POJO 的 JSON 绑定支持非常通用,允许从任何 Java 对象映射到 JSON。这是通过 Jackson 的org.codehaus.jackson.map.ObjectMapper实例完成的。例如,要在Coffee对象中读取 JSON,我们使用以下方式:

ObjectMapper objectMapper = new ObjectMapper();
Coffee coffee = objectMapper.readValue(jsonData, Coffee.class);

有关更多详细信息,请查看jersey.java.net/documentation/1.18/json.html

基于 JAXB 的 JSON 绑定支持

如果资源可以生成和消耗 XML 或 JSON,则基于 JAXB 的 JSON 绑定支持非常有用。要实现这一点,可以使用@XMLRootElement注释一个简单的 POJO,如下面的代码所示:

@XMLRootElement
public class Coffee {
    private String type;
    private String size;
}

使用前面的 JAXB bean 从资源方法生成 JSON 数据格式就像使用以下方式一样简单:

 @GET
 @Produces("application/json")
 public Coffee getCoffee() { 
     //Implementation goes here
}

Produces注解将负责将Coffee资源转换为 JSON 表示。

低级 JSON 解析和处理支持

这最适用于使用JSONArrayJSONObject获得对 JSON 格式的精细控制,以创建 JSON 表示。这里的优势在于应用程序开发人员将完全控制所生成和使用的 JSON 格式。以下是使用JSONArray的示例代码:

JsonObject myObject = Json.createObjectBuilder()
        .add("name", "Mocha")
        .add("size", "Large")
        .build();

另一方面,处理数据模型对象可能会更加复杂。例如,以下代码显示了拉取解析编程模型如何与 JSONParser 一起工作:

JsonParser parser = Json.createParser(…)
Event event = parser.next(); // START_OBJECT
event = parser.next(); //END OBJECT

下一节将介绍如何对 API 进行版本控制,以便它可以随着时间的推移而发展,并确保客户端应用程序的基本功能不会因服务器端 API 版本更改而中断。

API 版本控制

对于应用程序的演变,URI 设计应该有一些约束来识别版本。很难预见应用程序生命周期中将发生变化的所有资源。API 版本控制的目标是定义资源端点和寻址方案,并将版本与其关联。API 开发人员必须确保 HTTP 动词的语义和状态代码在版本更改时可以继续工作而无需人工干预。在应用程序的生命周期内,版本将会发展,API 可能需要被弃用。对于 API 的旧版本的请求可以重定向到最新的代码路径,或者可以使用适当的错误代码来指示 API 已过时。

可以有不同的方法来对 API 进行版本控制。这些方法如下:

  • 在 URI 本身中指定版本

  • 在请求查询参数中指定版本

  • Accept标头中指定版本

所有这些都可以正常工作。下一节将详细介绍方法并突出每种方法的优缺点。

URI 中的版本方法

在这种方法中,版本是服务器公开的资源的 URI 的一部分。

例如,在以下 URL 中,作为资源路径的一部分公开了“v2”版本:

http://api.foo.com/v2/coffees/1234

此外,API 开发人员可以提供一个路径,默认为最新版本的 API。因此,以下请求 URI 应该表现相同:

  • http://api.foo.com/coffees/1234

  • http://api.foo.com/v2/coffees/1234

这表示 v2 是最新的 API 版本。如果客户端指向旧版本,则应通知他们使用以下 HTTP 代码进行重定向以使用新版本:

  • 301 Moved permanently:这表示具有请求的 URI 的资源已永久移动到另一个 URI。此状态代码可用于指示旧的或不受支持的 API 版本,通知 API 客户端资源 URI 已被资源永久替换。

  • 302 Found:这表示所请求的资源暂时位于另一个位置,而所请求的 URI 可能仍然受支持。

作为请求查询参数的一部分的版本

使用 API 版本的另一种方式是将版本发送到请求参数中。资源方法可以根据请求中发送的版本选择代码流程。例如,在http://api.foo.com/coffees/1234?version=v2 URL 中,v2 已被指定为查询参数?version=v2的一部分。

这种格式的缺点是响应可能无法被缓存。此外,资源实现的源代码将根据查询参数中的版本而有不同的流程,这并不直观或易于维护。

注意

有关缓存最佳实践的更多详细信息将在第四章中进行介绍,性能设计

相比之下,如果 URI 包含版本信息,那么它会更清晰、更易读。此外,URI 的版本可能有一个标准的生命周期,在此之后,对于旧版本的所有请求都会重定向到最新版本。

注意

Facebook、Twitter 和 Stripe API 都将版本作为 URI 的一部分。Facebook API 在发布后两年内使版本不可用。如果客户端进行未版本化的调用,服务器将默认使用 Facebook API 的最早可用版本。

Twitter API 提供了六个月的时间来完全从 v1.0 过渡到 v1.1。

有关这些 API 的更多详细信息将在附录中找到。

Accept头中指定版本

一些 API 更喜欢将版本作为Accept头的一部分。例如,看一下以下代码片段:

Accept: application/vnd.foo-v1+json

在上面的片段中,vnd代表特定于供应商的 MIME 类型。这会移除 URL 的版本,并且受到一些 API 开发者的青睐。

注意

GitHub API 建议您明确发送Accept头,如下所示:

Accept: application/vnd.github.v3+json

有关更多详细信息,请查看developer.github.com/v3/media/

下一节将介绍应该发送给客户端的标准 HTTP 响应代码。

响应代码和 REST 模式

HTTP 提供了可以针对每个请求返回的标准化响应代码。以下表格总结了基于 CRUD API 的 REST 响应模式。根据使用的操作以及是否将内容作为响应的一部分发送,会有细微的差异:

响应代码 描述
成功 2XX 200 OK 这可以用于使用PUTPOSTDELETE进行createupdatedelete操作。这会作为响应的一部分返回内容。
201 Created 这可以用于使用PUT创建资源时。它必须包含资源的Location头。
204 No Content 这可以用于DELETEPOSTPUT操作。响应中不返回任何内容。
202 Accepted 这会在处理尚未完成时稍后发送响应。这用于异步操作。这还应返回一个Location头,可以指定客户端可以监视请求的位置。
重定向 3XX 301 Permanent 这可以用于显示所有请求都被重定向到新位置。
302 Found 这可以用于显示资源已经存在且有效。
客户端错误 4XX 401 Unauthorized 这用于显示基于凭据无法处理请求。
404 Not Found 这用于显示资源未找到。最好的做法是对未经认证的请求返回404 Not Found错误,以防止信息泄漏。
406 Not Acceptable 这可以用于当资源无法生成客户端指定的 MIME 类型时。当Accept头中指定的 MIME 类型与使用@Produces注释的资源方法/类中的任何媒体类型不匹配时,就会发生这种情况。
415 不支持的媒体类型 当客户端发送无法被资源消耗的媒体类型时可以使用。当Content-Type标头中指定的 MIME 类型与@Consumes注释的资源方法/类中的任何媒体类型不匹配时会发生这种情况。
服务器错误 5XX 500 内部服务器错误 当没有特定细节可用时,这是一个通用的内部服务器错误消息。
503 服务不可用 当服务器正在维护或太忙无法处理请求时可以使用。

JAX-RS 定义了一个javax.ws.rs.core.Response类,该类具有使用javax.ws.rs.core.Response.ResponseBuilder创建实例的静态方法:

@POST
 Response addCoffee(...) {
   Coffee coffee = ...
   URI coffeeId = UriBuilder.fromResource(Coffee.class)...
   return Response.created(coffeeId).build();
 }

上述代码片段显示了一个addCoffee()方法,该方法使用Response.created()方法返回201 已创建响应。有关其他响应方法的更多详细信息,请查看jersey.java.net/apidocs/latest/jersey/javax/ws/rs/core/Response.html

推荐阅读

摘要

在本章中,我们涵盖了内容协商、API 版本控制和 REST 响应代码等主题。本章的一个主要要点是要理解支持同一资源的各种表示形式有多么重要,以便客户端可以为其情况选择合适的表示形式。我们涵盖了流式传输和分块输出之间的差异,以及它们如何作为轻量级选项与自定义实体提供者(如MessageBodyReaderMessageBodyWriter)一起使用。我们看到了一些公司在其解决方案中使用版本控制的案例研究,以及在各种主题中散布的最佳实践和设计原则。

下一章将涵盖 REST 编程模型中的高级细节,如安全性、可追溯性和验证。

第三章:安全性和可追溯性

在开放平台时代,开发人员可以构建应用程序,这些应用程序可以很容易地并快速地与平台的业务周期解耦。这种基于 API 的架构实现了敏捷开发、更容易的采用、普及和规模化,并与企业内外的应用程序集成。应用程序的最重要考虑因素之一是处理安全性。构建应用程序的开发人员不应该关心用户的凭据。此外,还可以有其他客户端使用 REST 服务,包括但不限于浏览器和移动应用程序到其他服务。客户端可以代表其他用户执行操作,并且必须经过授权才能代表他们执行操作,而无需用户共享用户名和密码。这就是 OAuth 2.0 规范的作用所在。

构建分布式应用程序时需要考虑的另一个重要方面是可追溯性,这将涉及记录与请求相关的数据,以进行调试,这些请求在涵盖多个微服务的环境中可能是地理分布的,并且处理成千上万的请求。必须记录对 REST 资源的请求和状态代码,以帮助调试生产中的问题,并且还可以作为审计跟踪。本章将涵盖 REST 编程模型中安全性和可追溯性的高级细节。涵盖的主题如下:

  • 记录 REST API

  • RESTful 服务的异常处理

  • 验证模式

  • 联合身份

  • SAML 2.0

  • OAuth 2.0

  • OpenID Connect

本章将总结构建可扩展、高性能的 RESTful 服务所需的各种构建块。

记录 REST API

复杂的分布式应用程序可能会引入许多故障点。问题很难找到和修复,因此延迟了事件响应并造成了昂贵的升级。应用程序开发人员和管理员可能无法直接访问他们所需的机器数据。

记录是构建 RESTful 服务的一个非常重要的方面,特别是在调试运行各种微服务的分布式节点中出现生产问题的情况下。它有助于链接构成应用程序或业务服务的各个组件之间的事件或事务。完整的日志序列可以帮助重现在生产系统中发生的事件过程。此外,日志还可以帮助索引、聚合、切片数据、分析请求模式,并提供大量潜在有用的信息。

以下代码涵盖了如何编写一个简单的日志记录过滤器,可以与 REST 资源集成。该过滤器将记录与请求相关的数据,如时间戳、查询字符串和输入:

@WebFilter(filterName = "LoggingFilter",
        urlPatterns = {"/*"}
)
public class LoggingFilter implements Filter {
    static final Logger logger = Logger.getLogger(LoggingFilter.class);
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;

logger.info("request" +httpServletRequest.getPathInfo().toString());
        filterChain.doFilter(servletRequest, servletResponse);

    }

LoggingFilter类是一个简单的过滤器,实现了javax.servlet.Filter接口。记录器将记录所有带有请求路径和输入的消息。示例使用 Apache Log4j 设置日志记录。

注意

有关 Apache Log4J 的更多详细信息,请查看logging.apache.org/log4j/2.x/

然后可以从分布式日志服务器应用程序(例如 Splunk (www.splunk.com/)中收集和挖掘这些日志,这可以为开发人员提供有关生产中故障或性能问题的信息和根本原因分析。在我们的咖啡店类比中,一个例子是处理咖啡订单时出现问题。如果请求细节被记录在 Splunk 等分布式日志服务器应用程序中,开发人员可以根据时间查询,并查看客户端尝试发送的内容以及请求失败的原因。

下一节将涵盖许多在记录 REST API 时要牢记的最佳实践。

记录 REST API 的最佳实践

在大规模分布式环境中,日志数据可能是开发人员用于调试问题的唯一信息。如果审计和日志记录做得好,可以极大地帮助解决此类生产问题,并重放出问题发生前的步骤序列。以下部分列出了一些用于理解系统行为和性能等问题的日志记录最佳实践。

在服务日志中包括详细的一致模式

记录模式至少应包括以下内容是一个良好的实践:

  • 日期和当前时间

  • 记录级别

  • 线程的名称

  • 简单的记录器名称

  • 详细的消息

混淆敏感数据

在生产日志中掩盖或混淆敏感数据非常重要,以保护泄露机密和关键客户信息的风险。密码混淆器可以在日志过滤器中使用,它将从日志中掩盖密码、信用卡号等。个人可识别信息PII是指可以单独使用或与其他信息一起用于识别个人的信息。PII 的例子可以是一个人的姓名、电子邮件、信用卡号等。表示 PII 的数据应该使用各种技术进行掩盖,如替换、洗牌、加密等技术。

注意

更多详情,请查看en.wikipedia.org/wiki/Data_masking

识别调用者或发起者作为日志的一部分

在日志中标识调用者是一个良好的实践。API 可能被各种客户端调用,例如移动端、Web 端或其他服务。添加一种方式来识别调用者可能有助于调试问题,以防问题特定于某个客户端。

默认情况下不记录有效负载

具有可配置选项以记录有效负载,以便默认情况下不记录任何有效负载。这将确保对于处理敏感数据的资源,在默认情况下不会记录有效负载。

识别与请求相关的元信息

每个请求都应该有一些关于执行请求所花费的时间、请求的状态和请求的大小的细节。这将有助于识别延迟问题以及可能出现的大消息的其他性能问题。

将日志系统与监控系统绑定

确保日志中的数据也可以与监控系统绑定,后者可以在后台收集与 SLA 指标和其他统计数据相关的数据。

注意

各种平台上分布式环境中日志框架的案例研究

Facebook 开发了一个名为 Scribe 的自制解决方案,它是一个用于聚合流式日志数据的服务器。它可以处理全球分布的服务器每天大量的请求。服务器发送数据,可以进行处理、诊断、索引、汇总或聚合。Scribe 被设计为可以扩展到非常大量的节点。它被设计为能够经受住网络和节点故障的考验。系统中的每个节点都运行着一个 scribe 服务器。它被配置为聚合消息,并将它们发送到一个更大的组中的中央 scribe 服务器。如果中央 scribe 服务器宕机,消息将被写入本地磁盘上的文件,并在中央服务器恢复时发送。更多详情,请查看github.com/facebookarchive/scribe

Dapper 是谷歌的跟踪系统,它从成千上万的请求中采样数据,并提供足够的信息来跟踪数据。跟踪数据被收集在本地日志文件中,然后被拉入谷歌的 BigTable 数据库。谷歌发现对于常见情况采样足够的信息可以帮助跟踪细节。更多详情,请查看research.google.com/pubs/pub36356.html

接下来的部分将介绍如何验证 REST API 请求和/或响应实体。

验证 RESTful 服务

在暴露 REST 或基于 HTTP 的服务 API 时,验证 API 的行为是否正确以及暴露的数据格式是否按预期结构化是很重要的。例如,验证 RESTful 服务的输入,例如作为请求体发送的电子邮件,必须符合标准,负载中必须存在某些值,邮政编码必须遵循特定格式等。这可以通过 RESTful 服务的验证来完成。

JAX-RS 支持 Bean 验证来验证 JAX-RS 资源类。这种支持包括:

  • 向资源方法参数添加约束注释

  • 确保在将实体作为参数传递时实体数据有效

以下是包含@Valid注释的CoffeesResource类的代码片段:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @ValidateOnExecution
    public Response addCoffee(@Valid Coffee coffee) {
        …
            }

javax.validation.executable.ValidateOnExecution注释可以帮助指定哪个方法或构造函数应在执行时验证其参数和返回值。请求体上的javax.validation.Valid注释将确保Coffee对象将符合 POJO 中指定的规则。

以下是Coffee POJO 的代码片段:

@XmlRootElement
public class Coffee {

    @VerifyValue(Type.class)
    private String type;

    @VerifyValue(Size.class)
    private String size;

    @NotNull
    private String name;
    // getters and setters
}

字段名具有javax.validation.constrains.NotNull注释,强制要求订单中的咖啡名称不能为空。同样,我们在示例中定义了自定义注释,它将验证类型和大小,并检查请求体中的值是否遵循正确的格式。

例如,Size可以是以下值之一:SmallMediumLargeExtraLarge

public enum Size {
    Small("S"), Medium("M"), Large("L"), ExtraLarge("XL");
    private String value;
}

@VerifyValue(Size.class)注释是在可下载示例中定义的自定义注释。

验证异常处理和响应代码

以下表格提供了在抛出各种与验证相关的异常时返回的响应代码的快速摘要。错误代码的类型取决于抛出的异常以及验证是在 HTTP 方法的请求还是响应上执行的。

返回的 HTTP 响应代码 异常类型
500 内部服务器错误 当验证方法返回类型时抛出javax.validation.ValidationExceptionValidationException的任何子类,包括ConstraintValidationException时返回此错误代码
400 错误 当在验证方法中抛出ConstraintViolationException以及所有其他情况时

接下来的部分涵盖了 API 开发人员如何抛出特定于应用程序的异常,并根据异常映射 HTTP 错误代码。

RESTful 服务的错误处理

在构建 RESTful API 时,需要抛出特定于应用程序的异常,并提供包含这些异常详细信息的特定 HTTP 响应。接下来的部分将介绍如何处理用户定义的异常并将它们映射到 HTTP 响应和状态代码。javax.ws.rs.ext.ExceptionMapper类是自定义的、应用程序提供的组件,它捕获抛出的应用程序异常并编写特定的 HTTP 响应。异常映射器类使用@Provider注释进行标注。

以下代码片段显示了如何构建自定义异常映射器:

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/orders/{id}")
    public Response getCoffee(@PathParam("id") int id) {
        Coffee coffee =  CoffeeService.getCoffee(id);
        if (coffee == null)
            throw new CoffeeNotFoundException("No coffee found for order " + id);
        return Response.ok(coffee).type(MediaType.APPLICATION_JSON_TYPE).build();
    }

如前面的代码片段所示,getCoffees()方法返回一个带有指定路径参数的Coffee对象。如果找不到指定 ID 的咖啡,则代码会抛出CoffeeNotFoundException

以下是ExceptionMapper类实现的代码:

@Provider
public class MyExceptionMapper implements ExceptionMapper<Exception> {

    public Response toResponse(Exception e) {
        ResourceError resourceError = new ResourceError();

        String error = "Service encountered an internal error";
        if (e instanceof CoffeeNotFoundException) {
            resourceError.setCode(Response.Status.NOT_FOUND.getStatusCode());
            resourceError.setMessage(e.getMessage());

            return Response.status(Response.Status.NOT_FOUND).entity(resourceError)
                    .type(MediaType.APPLICATION_JSON_TYPE)
                    .build();
        }
        return Response.status(503).entity(resourceError).type(MediaType.APPLICATION_JSON_TYPE)
                .build();
    }
}

前面的代码显示了ExceptionMapper的实现,其toResponse()方法已被覆盖。代码检查抛出的异常是否是CoffeeNotFoundException的实例,然后返回一个实体类型为ResourceError的响应。

ResourceError类是一个使用@XMLRootElement注释的 POJO,并作为响应的一部分发送:

@XmlRootElement
public class ResourceError {

    private int code;
    private String message;
    //getters and setters
…}

您可以将示例作为可下载包的一部分运行,输出如下:

HTTP/1.1 404 Not Found
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.0  Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition 4.0
Content-Type: application/json
Content-Length: 54

{"code":404,"message":"No coffee found for order 100"}

认证和授权

过去,组织需要一种方式来统一企业用户的身份验证。单点登录是一个解决方案,可以在企业的不同应用程序中保持一个用户名和密码的存储库。

随着面向服务的架构的发展,组织需要一种方式,使合作伙伴和其他服务可以使用 API,并且需要一种简化各种应用程序和平台之间登录过程的方式。随着社交媒体的发展,各种平台开放,API 和生态系统建立了大量应用程序和大量设备使用 Twitter、Facebook 和 LinkedIn 等平台。

因此,将认证和授权功能与消费应用程序解耦变得越来越重要。此外,并非每个应用程序都必须知道用户的凭据。接下来的部分将涵盖 SAML 2.0 和 OAuth 2.0,作为简化登录和增加安全性的联合身份的一部分。

子节将枚举以下主题:

  • SAML

  • OAuth

  • 刷新令牌与访问令牌

  • Jersey 和 OAuth 2.0

  • 何时使用 SAML 或 OAuth?

  • OpenID Connect

什么是认证?

认证是建立和传达操作浏览器或本机应用程序的人是他/她声称的人的过程。

SAML

安全断言标记语言SAML)是一个标准,包括配置文件、绑定和构造,以实现单点登录SSO)、联合和身份管理。

SAML 2.0 规范提供了 Web 浏览器 SSO 配置文件,定义了如何实现 Web 应用程序的单点登录。它定义了三个角色:

  • 主体:这通常是用户想要验证自己的身份的地方

  • 身份提供者IdP):这是能够验证最终用户身份的实体

  • 服务提供者SP):这是希望使用身份提供者验证最终用户身份的实体

以下流程显示了 SAML 的一个简单示例。比如,员工想要访问企业旅行网站。企业旅行应用程序将请求与员工关联的身份提供者来验证他的身份,然后为他采取行动。

SAML

流程解释如下:

  1. 用户访问企业应用程序,比如旅行应用程序。

  2. 旅行应用程序将生成一个 SAML 请求,并将用户重定向到雇主的身份提供者IdP)。

  3. 用户被重定向到雇主的身份提供者以获取 SAML 认证断言。

  4. IdP 解析 SAML 请求,对用户进行身份验证,并生成 SAML 响应。

  5. 浏览器将 SAML 响应发送到旅行应用程序。

  6. 收到访问令牌后,企业旅行应用程序随后能够通过在 HTTP 请求的标头中传递令牌来访问 Web 资源。访问令牌充当一个会话令牌,封装了旅行应用程序代表用户的事实。

SAML 具有用于 Web 浏览器、SSO、SOAP 和 WS-Security 的绑定规范,但没有正式的 REST API 绑定。

下一节涵盖了 OAuth,这已被 Twitter、Facebook 和 Google 等平台广泛使用于授权。

什么是授权?

授权是检查请求者是否有权限执行所请求操作的过程。

OAuth

OAuth 代表开放授权,为用户授权应用程序访问其与账户相关的数据提供了一种方式,而不需要提供用户名和密码。

在客户端/服务器身份验证中,客户端使用其凭据访问服务器上的资源。服务器不在乎请求是来自客户端还是客户端是否为其他实体请求资源。实体可以是另一个应用程序或另一个人,因此客户端不是在访问自己的资源,而是在访问另一个用户的资源。请求访问受保护且需要身份验证的资源的任何人都必须得到资源所有者的授权。OAuth 是一种打开 Twitter、Facebook、Google+、GitHub 等公司的 REST API 以及建立在其之上的众多第三方应用程序的方法。OAuth 2.0 完全依赖于 SSL。

OAuth 请求中的步数指涉及的参与方数量。客户端、服务器和资源所有者都参与的流程表示 3-legged OAuth。当客户端代表自己行事时,它被称为 2-legged OAuth。

OAuth 通过访问令牌实现此功能。访问令牌就像提供有限功能的代客泊车钥匙,可以在有限的时间内访问。令牌的寿命有限,从几小时到几天不等。以下图表显示了 OAuth 的流程:

OAuth

上述图表显示了授权代码授予流程。

在这个例子中,用户在服务提供商网站上有他的照片,比如 Flickr。现在,用户需要调用打印服务来打印他的照片,例如 Snapfish,这是一个消费者应用程序。用户可以使用 OAuth 允许打印服务在有限的时间内访问他的照片,而不是将他的用户名和密码分享给消费者应用程序。

因此,在我们的示例中,有三个角色,如下所述:

  • 用户或资源所有者:用户是希望打印他的照片的资源所有者

  • 消费者应用程序或客户端:这是打印服务应用程序,将代表用户行事

  • 服务提供商或服务器:服务提供商是将存储用户照片的资源服务器

有了这个例子,我们可以看到 OAuth 舞蹈中涉及的步骤:

  1. 用户希望允许应用程序代表他执行任务。在我们的例子中,任务是打印照片,这些照片在服务器上使用消费者应用程序。

  2. 消费者应用程序将用户重定向到服务提供商的授权 URL。

在这里,提供者显示一个网页,询问用户是否可以授予应用程序读取和更新其数据的访问权限。

  1. 用户同意通过打印服务消费者应用程序授予应用程序访问权限。

  2. 服务提供商将用户重定向回应用程序(通过重定向 URI),将授权代码作为参数传递。

  3. 应用程序将授权代码交换为访问授权。服务提供商向应用程序发放访问授权。授权包括访问令牌和刷新令牌。

  4. 现在连接建立,消费者应用程序现在可以获取对服务 API 的引用,并代表用户调用提供者。因此,打印服务现在可以从服务提供商的网站访问用户的照片。

注意

OAuth 的优势在于,由于使用访问令牌而不是实际凭据,受损的应用程序不会造成太多混乱。 SAML 承载流实际上与之前介绍的经典 OAuth 3-leg 流非常相似。但是,与将用户的浏览器重定向到授权服务器不同,服务提供商与身份提供商合作以获得简单的身份验证断言。服务提供商应用程序为用户交换 SAML 承载断言,而不是交换授权代码。

OAuth 2.0 和 OAuth 1.0 之间的区别

OAuth 2.0 规范清楚地阐述了如何完全在浏览器中使用 JavaScript 使用 OAuth,而没有安全地存储令牌的方法。这还在高层次上解释了如何在手机上或甚至在根本没有网络浏览器的设备上使用 OAuth,涵盖了对智能手机和传统计算设备上的应用程序本机应用程序的交互,以及网站。

OAuth 2.0 定义了以下三种类型的配置文件:

  • Web 应用程序(在这种情况下,客户端密码存储在服务器上,并且使用访问令牌。)

  • Web 浏览器客户端(在这种情况下,不信任 OAuth 凭据;一些提供商不会发布客户端密钥。一个例子是浏览器中的 JavaScript。)

  • 本机应用程序(在这种情况下,生成的访问令牌或刷新令牌可以提供可接受的保护级别。一个例子包括移动应用程序。)

OAuth 2.0 不需要加密,使用的是 HTTPS 而不是 HMAC。此外,OAuth 2.0 允许限制访问令牌的生命周期。

授权授予

授权授予是代表资源所有者或用户授权的凭据,允许客户端访问其受保护的资源以获取访问令牌。OAuth 2.0 规范定义了四种授权类型,如下所示:

  • 授权码授予

  • 隐式授予

  • 资源所有者密码凭据授予

  • 客户端凭据授予

此外,OAuth 2.0 还定义了用于定义其他类型的可扩展机制。

刷新令牌与访问令牌

刷新令牌是用于获取访问令牌的凭据。当当前访问令牌无效或过期时,刷新令牌用于获取访问令牌。发放刷新令牌是服务器自行决定的可选项。

与访问令牌不同,刷新令牌仅用于与授权服务器一起使用,永远不会发送到资源服务器以访问资源。

Jersey 和 OAuth 2.0

尽管 OAuth 2.0 被各个企业广泛使用,但 OAuth 2.0 RFC 是在其基础上构建解决方案的框架。在 RFC 中有许多灰色地带,规范留给实施者。在没有必需的令牌类型、令牌过期协议或令牌大小的具体指导的领域存在犹豫。

注意

阅读此页面以获取更多详细信息:

hueniverse.com/2012/07/26/oauth-2-0-and-the-road-to-hell/

目前,Jersey 对 OAuth 2.0 的支持仅限于客户端。OAuth 2.0 规范定义了许多扩展点,由服务提供商来实现这些细节。此外,OAuth 2.0 定义了多个授权流程。授权码授予流程是 Jersey 目前支持的流程,其他流程都不受支持。有关更多详细信息,请查看jersey.java.net/documentation/latest/security.html

REST API 中 OAuth 的最佳实践

以下部分列出了服务提供商实施 OAuth 2.0 可以遵循的一些最佳实践。

限制访问令牌的生命周期

协议参数expires_in允许授权服务器限制访问令牌的生命周期,并将此信息传递给客户端。此机制可用于发行短期令牌。

支持在授权服务器中提供刷新令牌

刷新令牌可以与短期访问令牌一起发送,以授予对资源的更长时间访问,而无需涉及用户授权。这提供了一个优势,即资源服务器和授权服务器可能不是同一实体。例如,在分布式环境中,刷新令牌总是在授权服务器上交换。

使用 SSL 和加密

OAuth 2.0 严重依赖于 HTTPS。这将使框架更简单但不太安全。

以下表格提供了何时使用 SAML 和何时使用 OAuth 的快速摘要。

场景 SAML OAuth
如果参与方之一是企业 使用 SAML
如果应用程序需要为某些资源提供临时访问权限 使用 OAuth
如果应用程序需要自定义身份提供者 使用 SAML
如果应用程序有移动设备访问 使用 OAuth
如果应用程序对传输没有限制,例如 SOAP 和 JMS 使用 SAML

OpenID Connect

OpenID 基金会正在进行 OpenID Connect 的工作。OpenID Connect 是建立在 OAuth 2.0 之上的简单的基于 REST 和 JSON 的可互操作协议。它比 SAML 更简单,易于维护,并覆盖了从社交网络到商业应用程序再到高度安全的政府应用程序的各种安全级别。OpenID Connect 和 OAuth 是身份验证和授权的未来。有关更多详细信息,请访问openid.net/connect/

注意

使用 OAuth 2.0 和 OpenID Connect 的公司案例

Google+登录是建立在 OAuth 2.0 和 OpenID Connect 协议之上的。它支持空中安装、社交功能,并在标准化的 OpenID Connect 登录流程之上提供登录小部件。

接下来的部分将总结到目前为止我们在构建 RESTful 服务时涵盖的各种组件。

REST 架构组件

接下来的部分将涵盖在构建 RESTful API 时必须考虑的各种组件。所有这些将在本书的各个部分中进行介绍。我们还将介绍在设计和开发 REST API 时要避免的各种陷阱的最佳实践。REST 架构组件如下图所示:

REST 架构组件

从上图中可以看到,REST 服务可以从各种客户端和运行在不同平台和设备上的应用程序中消耗,例如移动设备和 Web 浏览器。

这些请求通过代理服务器发送。如前图所示,可以将图中的 REST 架构组件链接在一起。例如,可以有一个过滤器链,包括Auth速率限制缓存日志记录相关的过滤器。这将负责对用户进行身份验证,检查来自客户端的请求是否在速率限制内,然后是一个缓存过滤器,可以检查请求是否可以从缓存中提供。接下来是一个日志记录过滤器,可以记录请求的详细信息。

在响应端,可以进行分页,以确保服务器发送结果的子集。此外,服务器可以进行异步处理,从而提高响应能力和规模。响应中可以包含链接,处理 HATEOAS。

这些是我们迄今为止涵盖的一些 REST 架构组件:

  • 使用 HTTP 请求使用 HTTP 动词来使用 REST API 进行统一接口约束

  • 内容协商,在存在多个表示可用时选择响应的表示

  • 日志记录以提供可追溯性以分析和调试问题

  • 异常处理以使用 HTTP 代码发送特定于应用程序的异常

  • 使用 OAuth 2.0 进行身份验证和授权,以便为其他应用程序提供访问控制,并在用户无需发送其凭据的情况下执行操作

  • 验证以向客户端发送详细的带有错误代码的消息,以及对请求中收到的输入进行验证

接下来的几章将重点介绍高级主题以及以下模块的最佳实践。我们将提供代码片段,以展示如何使用 JAX-RS 实现这些功能。

  • 速率限制以确保服务器不会因来自单个客户端的太多请求而负担过重

  • 缓存以提高应用程序的响应能力

  • 异步处理,使服务器可以异步地向客户端发送响应

  • 微服务将单片服务分解为细粒度服务

  • HATEOAS 通过在响应中返回链接列表来改善可用性、可理解性和可导航性

  • 分页,允许客户端指定感兴趣的数据集中的项目

我们还将介绍主要平台,如 Facebook、Google、GitHub 和 PayPal 是如何在其 REST API 中采用这些解决方案的。

推荐阅读

以下链接可能对获取与本章主题相关的额外信息有用:

总结

本章以对记录 RESTful API 进行简要介绍开始,关键原则是认识到记录请求的重要性以及记录的最佳实践,包括安全合规性。我们学习了如何使用 Bean Validation 验证 JAX-RS 2.0 资源。在本章中,我们还看到了如何为特定应用程序情况编写通用异常映射器。

我们讨论了联合身份在当前互联混合系统、协议和设备时代的必要性。我们讨论了 SAML 和 OAuth 2.0 之间的相似之处,以及 3-legged OAuth 和 OAuth 的最佳实践。

下一章将介绍诸如缓存模式和异步 REST API 以提高性能和可伸缩性,然后更详细地了解如何使用 HTTP Patch 和更新 JSON Patch 执行部分更新。

第四章:性能设计

REST 是一种符合 Web 架构设计的架构风格,需要正确设计和实现,以便利用可扩展的 Web。本章涵盖了与性能相关的高级设计原则,每个开发人员在构建 RESTful 服务时都必须了解。

本章涵盖的主题包括以下内容:

  • 缓存原则

  • REST 中的异步和长时间运行的作业

  • HTTP PATCH 和部分更新

我们将详细介绍不同的 HTTP 缓存头,并学习如何发送条件请求,以查看新内容或缓存内容是否需要返回。然后,我们将展示如何使用 JAX-RS 来实现缓存。

此外,我们将介绍 Facebook API 如何使用 ETags 进行缓存。接下来,我们将介绍如何使用 JAX-RS 进行异步请求响应处理以及最佳实践。最后,我们将介绍 HTTP PATCH 方法,并学习如何实现部分更新以及部分更新的常见实践。

本章包含了不同的代码片段,但展示这些片段在实际中的完整示例包含在本书的源代码下载包中。

缓存原则

在本节中,我们将介绍设计 RESTful 服务时涉及的不同编程原则。我们将涵盖的一个领域是缓存。缓存涉及将与请求相关的响应信息存储在临时存储中,以特定时间段内。这确保了服务器在未来不需要处理这些请求时,可以从缓存中满足响应。

缓存条目可以在特定时间间隔后失效。缓存条目也可以在缓存中的对象发生变化时失效,例如,当某个 API 修改或删除资源时。

缓存有许多好处。缓存有助于减少延迟并提高应用程序的响应速度。它有助于减少服务器需要处理的请求数量,因此服务器能够处理更多的请求,客户端将更快地获得响应。

通常,诸如图像、JavaScript 文件和样式表等资源都可以被相当大地缓存。此外,建议缓存可能需要在后端进行密集计算的响应。

缓存细节

接下来的部分涵盖了与缓存相关的主题。使缓存有效工作的关键是使用 HTTP 缓存头,指定资源的有效时间以及上次更改的时间。

缓存头的类型

下一节将介绍缓存头的类型,然后是每种缓存头的示例。以下是头部的类型:

  • 强缓存头

  • 弱缓存头

强缓存头

强缓存头指定了缓存资源的有效时间,浏览器在此期间不需要发送任何更多的GET请求。ExpiresCache-Control max-age是强缓存头。

弱缓存头

弱缓存头帮助浏览器决定是否需要通过发出条件GET请求从缓存中获取项目。Last-ModifiedETag是弱缓存头的示例。

Expires 和 Cache-Control - max-age

ExpiresCache-Control头指定了浏览器可以在不检查更新版本的情况下使用缓存资源的时间段。如果设置了这些头部,直到到期日期或达到最大年龄为止,新资源将不会被获取。Expires头部接受一个日期,指定资源失效的时间。而max-age属性则指定资源在下载后的有效时间。

缓存控制头和指令

HTTP 1.1中,Cache-Control头指定了资源的缓存行为以及资源可以被缓存的最大年龄。以下表格显示了Cache-Control头的不同指令:

指令 意义
private 当使用此指令时,浏览器可以缓存对象,但代理和内容交付网络不能
public 当使用此指令时,浏览器、代理和内容交付网络可以缓存对象
no-cache 当使用此指令时,对象将不被缓存
no-store 当使用此选项时,对象可以被缓存在内存中,但不应存储在磁盘上
max-age 表示资源有效的时间

以下是带有Cache-Control HTTP/1.1头的响应的示例:

HTTP/1.1 200 OK Content-Type: application/json
Cache-Control: private, max-age=86400
Last-Modified: Thur, 01 Apr 2014 11:30 PST

前面的响应具有Cache-Control头,指令为privatemax-age设置为 24 小时或 86400 秒。

一旦资源基于max-ageExpires头无效,客户端可以再次请求资源或发送条件GET请求,只有在资源发生更改时才获取资源。这可以通过较弱的缓存头来实现:如下一节所示的Last-Modified和 ETag 头。

Last-Modified 和 ETag

这些头使浏览器能够检查资源自上次GET请求以来是否发生了更改。在Last-Modified头中,有一个与资源修改相关的日期。在 ETag 头中,可以有任何唯一标识资源的值(如哈希)。然而,这些头允许浏览器通过发出条件GET请求有效地更新其缓存资源。条件GET请求只有在服务器上的资源发生更改时才会返回完整的响应。这确保条件GET请求的延迟低于完整的GET请求。

Cache-Control 头和 REST API

以下代码显示了如何向 JAX-RS 响应添加Cache-Control头。该示例可作为本书可下载源代码包的一部分。

@Path("v1/coffees")
public class CoffeesResource {

    @GET
    @Path("{order}")
    @Produces(MediaType.APPLICATION_XML)
    @NotNull(message = "Coffee does not exist for the order id requested")
    public Response getCoffee(@PathParam("order") int order) {
        Coffee coffee = CoffeeService.getCoffee(order);
        CacheControl cacheControl = new CacheControl();
        cacheControl.setMaxAge(3600);
        cacheControl.setPrivate(true);
        Response.ResponseBuilder responseBuilder = Response.ok(coffee);
        responseBuilder.cacheControl(cacheControl);
        return responseBuilder.build();

    }

JAX-RS 有一个javax.ws.rs.core.Cache-Control类,它是HTTP/1.1 Cache-Control头的抽象。cacheControl对象上的setMaxAge()方法对应于max-age指令,setPrivate(true)对应于private指令。响应是使用responseBuilder.build()方法构建的。cacheControl对象被添加到getCoffee()方法返回的Response对象中。

以下是此应用程序生成的带有头的响应:

curl -i http://localhost:8080/caching/v1/coffees/1
HTTP/1.1 200 OK
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.0  Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition  4.0 
Cache-Control: private, no-transform, max-age=3600
Content-Type: application/xml
Date: Thu, 03 Apr 2014 06:07:14 GMT
Content-Length: 143

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<coffee>
<name>Mocha</name>
<order>1</order>
<size>Small</size>
<type>Chocolate</type>
</coffee>

ETags

HTTP 定义了一个强大的缓存机制,其中包括以下头部:

  • ETag

  • If-Modified-Since

  • 304 Not Modified响应代码

ETags 工作原理

以下部分深入介绍了 ETags 的一些基础知识。以下图表更好地展示了这一点:

ETags 工作原理

让我们来看看与 ETags 相关的每个过程:

  1. 客户端向api.com/coffee/1234 REST 资源发送一个GET请求。

  2. 服务器发送一个带有ETag值的200 OK,例如,"123456789"

  3. 过一段时间,客户端发送另一个带有If-None-Match: "123456789"头的GET请求到api.com/coffee/1234 REST 资源。

  4. 服务器检查资源的 MD5 哈希是否未被修改,然后发送一个没有响应主体的304 Not-Modified响应。

如果资源已更改,将发送 200 OK 作为响应。此外,作为响应的一部分,服务器还发送了一个新的 ETag。

ETag 头和 REST API

以下代码显示了如何向 JAX-RS 响应添加ETag头:

    @GET
    @Path("/etag/{order}")
    @Produces(MediaType.APPLICATION_JSON)
    @NotNull(message = "Coffee does not exist for the order id requested")
    public Response getCoffeeWithEtag(@PathParam("order") int order,
                                      @Context Request request
    ) {
        Coffee coffee = CoffeeService.getCoffee(order);
        EntityTag et = new EntityTag(
 "123456789");
        Response.ResponseBuilder responseBuilder  = request.evaluatePreconditions(et);
        if (responseBuilder != null) {
            responseBuilder.build();
        }
        responseBuilder = Response.ok(coffee);
        return responseBuilder.tag(et).build();

在上述代码片段中,使用资源的哈希创建了javax.ws.core.EntityTag对象的实例,为简单起见,我们使用了"123456789"。

request,evalautePreconditions 方法检查 EntityTag et 对象的值。如果满足先决条件,它将返回一个带有 200 OK 的响应。

然后,EntityTag 对象 et 与响应一起发送,该响应由 getCoffeeWithETag 方法返回。有关更多详细信息,请参考书籍源代码包中提供的示例。

ETags 的类型

强验证 ETag 匹配表示两个资源的内容是逐字节相同的,并且所有其他实体字段(例如 Content-Language)也没有更改。

弱验证 ETag 匹配仅表示两个资源在语义上是等价的,并且可以使用缓存的副本。

缓存有助于减少客户端发出的请求次数。它还有助于通过条件 GET 请求和 ETags、IF-None-Match 头和 304-Not Modified 响应来减少完整响应的数量,从而节省带宽和计算时间。

提示

在 HTTP 响应中指定 ExpiresCache-Control max-age 以及两者中的一个 Last-Modified 和 ETag 头是一个很好的做法。同时发送 ExpiresCache-Control max-age 是多余的。同样,发送 Last-Modified 和 ETag 也是多余的。

Facebook REST API 和 ETags

Facebook 营销 API 支持 Graph API 上的 ETags。当消费者进行 Graph API 调用时,响应头包括一个 ETag,其值是在 API 调用返回的数据的哈希值。下次消费者进行相同的 API 调用时,他可以在请求头中包含从第一步保存的 ETag 值的 If-None-Match 请求头。如果数据没有更改,响应状态码将是 304 - Not Modified,并且不返回数据。

如果服务器端的数据自上次查询以来发生了变化,则数据将像往常一样返回,并附带一个新的 ETag。这个新的 ETag 值可以用于后续调用。有关更多详细信息,请查看 developers.facebook.com

RESTEasy 和缓存

RESTEasy 是 JBoss 项目,提供各种框架来帮助构建 RESTful web 服务和 RESTful Java 应用程序。RESTEasy 可以在任何 servlet 容器中运行,但与 JBoss 应用服务器有更紧密的集成。

RESTEasy 提供了一个 JAX-RS 的扩展,允许在成功的 GET 请求上自动设置 Cache-Control 头。

它还提供了一个服务器端的本地内存缓存,可以位于 JAX-RS 服务的前面。如果 JAX-RS 资源方法设置了 Cache-Control 头,则它会自动缓存来自 HTTP GET JAX-RS 调用的编组响应。

HTTP GET 请求到达时,RESTEasy 服务器缓存将检查 URI 是否存储在缓存中。如果是,则返回已经编组的响应,而不调用 JAX-RS 方法。

有关更多信息,请查看 www.jboss.org/resteasy

提示

在服务器端进行缓存时的提示

对于 PUTPOST 请求,使缓存条目无效。不要缓存具有查询参数的请求,因为一旦查询参数值发生变化,来自服务器的缓存响应可能无效。

REST 中的异步和长时间运行的作业

在开发 RESTful API 中的一个常见模式是处理异步和长时间运行的作业。API 开发人员需要创建可能需要相当长时间的资源。他们不能让客户端等待 API 完成。

考虑在咖啡店订购咖啡。订单详细信息存储在队列中,当咖啡师有空时,他会处理您的订单。在那之前,您会收到一张收据确认您的订单,但实际的咖啡稍后到达。

异步资源处理遵循相同的原则。异步资源意味着资源不能立即创建。也许它将被放置在一个处理资源实际创建的任务/消息队列中,或者类似的东西。

考虑以下在我们示例中订购一杯小咖啡的请求:

POST v1/coffees/order HTTP 1.1 with body
<coffee>
  <size> SMALL</coffee>
  <name>EXPRESSO</name>
  <price>3.50</price>
<coffee>

响应可以发送回以下内容:

HTTP/1.1 202 Accepted
Location: /order/12345

响应发送一个202 Accepted头。Location头可以提供有关咖啡资源的详细信息。

异步请求和响应处理

异步处理包含在 JAX-RS 2.0 的客户端和服务器端 API 中,以促进客户端和服务器组件之间的异步交互。以下列表显示了添加到服务器端和客户端的新接口和类,以支持此功能:

  • 服务器端:

  • AsyncResponse:这是一个可注入的 JAX-RS 异步响应,提供了异步服务器端响应处理的手段

  • @Suspended@Suspended注解指示容器应在辅助线程中进行 HTTP 请求处理

  • CompletionCallback:这是一个接收请求处理完成事件的请求处理回调

  • ConnectionCallback:这是一个接收与连接相关的异步响应生命周期事件的异步请求处理生命周期回调

  • 客户端端:

  • InvocationCallback:这是一个可以实现的回调,用于接收调用处理的异步处理事件

  • Future:这允许客户端轮询异步操作的完成情况,或者阻塞并等待它

注意

Java SE 5 中引入的Future接口提供了两种不同的机制来获取异步操作的结果:首先是通过调用Future.get(…)变体来阻塞直到结果可用或超时发生,第二种方式是通过调用isDone()isCancelled()来检查完成情况,这些是返回Future当前状态的布尔方法。有关更多详细信息,请查看docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/Future.html

以下图表显示了 JAX-RS 中的异步请求/响应处理:

异步请求和响应处理

客户端发出对CoffeeResource上异步方法的请求。CoffeeResource类创建一个新线程,可以进行一些密集的操作,然后发送响应。同时,请求线程被释放,可以处理其他请求。当处理操作的线程完成处理时,将响应返回给客户端。

以下示例代码显示了如何使用 JAX-RS 2.0 API 开发异步资源:

@Path("/coffees")
@Stateless
public class CoffeeResource {   
  @Context private ExecutionContext ctx;
  @GET @Produce("application/json")
  @Asynchronous
  public void order() {
        Executors.newSingleThreadExecutor().submit( new Runnable() {
         public void run() { 
              Thread.sleep(10000);     
              ctx.resume("Hello async world! Coffee Order is 1234");
          } });
ctx.suspend();
return;
  }
}

CoffeesResource类是一个无状态会话 bean,其中有一个名为order()的方法。该方法带有@Asynchronous注解,将以“发出并忘记”的方式工作。当客户端通过order()方法的资源路径请求资源时,会生成一个新线程来处理准备请求的响应。线程被提交给执行程序执行,处理客户端请求的线程被释放(通过ctx.suspend)以处理其他传入的请求。

当为准备响应创建的工作线程完成准备响应时,它调用ctx.resume方法,让容器知道响应已准备好发送回客户端。如果在ctx.suspend方法之前调用了ctx.resume方法(工作线程在执行到达ctx.suspend方法之前已准备好结果),则会忽略暂停,并且结果将发送到客户端。

可以使用以下代码片段中显示的@Suspended注解来实现相同的功能:

@Path("/coffees")
@Stateless
public class CoffeeResource {
@GET @Produce("application/json")
@Asynchronous
  public void order(@Suspended AsyncResponse ar) {
    final String result = prepareResponse();
    ar.resume(result)
  }
}

使用@Suspended注解更清晰,因为这不涉及使用ExecutionContext变量来指示容器在工作线程完成时暂停然后恢复通信线程,即在这种情况下的prepareResponse()方法。消耗异步资源的客户端代码可以使用回调机制或在代码级别进行轮询。以下代码显示了如何通过Future接口进行轮询:

Future<Coffee> future = client.target("/coffees")
               .request()
               .async()
               .get(Coffee.class);
try {
   Coffee coffee = future.get(30, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
  System.err.println("Timeout occurred");
}

代码从形成对Coffee资源的请求开始。它使用javax.ws.rs.client.Client实例调用target()方法,该方法为Coffee资源创建一个javax.ws.rs.client.WebTarget实例。Future.get(…)方法会阻塞,直到从服务器收到响应或达到 30 秒的超时时间。

另一个用于异步客户端的 API 是使用javax.ws.rs.client.InvocationCallback实例,这是一个可以实现以获取调用的异步事件的回调。有关更多详细信息,请查看jax-rs-spec.java.net/nonav/2.0/apidocs/javax/ws/rs/client/InvocationCallback.html

异步资源最佳实践

下一节列出了在处理异步 RESTful 资源时的最佳实践。

发送 202 Accepted 消息

对于异步请求/响应,API 应该返回一个202 Accepted消息,以表明请求是有效的,资源可能在时间上是可用的,即使只有几秒钟。202 Accepted表示请求已被接受处理,资源将很快可用。202 Accepted消息应指定Location头,客户端可以使用它来知道资源创建后将在哪里可用。如果响应不立即可用,API 不应返回201 Created消息。

设置队列中对象的过期时间

API 开发人员应该在队列中的一定时间后使对象过期。这样可以确保队列对象不会随着时间的推移而积累,并且会定期清除。

使用消息队列来处理任务异步

API 开发人员应考虑使用消息队列来进行异步操作,以便消息被放置在队列中,直到接收者接收到它们。高级消息队列协议AMQP)是一种标准,它能够可靠和安全地路由、排队、发布和订阅消息。有关更多详细信息,请查看en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol上的高级消息队列协议。

例如,当调用异步资源方法时,使用消息队列发送消息,并根据消息和事件异步处理不同的任务。

在我们的示例中,如果下订单咖啡,可以使用 RabbitMQ(www.rabbitmq.com/)发送消息来触发COMPLETED事件。订单完成后,详细信息可以移至库存系统。

下一节涵盖了 RESTful 服务的另一个重要细节,即进行部分更新。

HTTP PATCH 和部分更新

API 开发人员常见的问题是实现部分更新。当客户端发送一个请求,必须改变资源状态的一部分时,就会出现这种情况。例如,想象一下,有一个 JSON 表示您的Coffee资源的代码片段如下所示:

{
 "id": 1,
 "name": "Mocha"
 "size": "Small",
 "type": "Latte",
 "status":"PROCESSING"
}

一旦订单完成,状态需要从"PROCESSING"更改为"COMPLETED"

在 RPC 风格的 API 中,可以通过添加以下方法来处理这个问题:

GET myservice/rpc/coffeeOrder/setOrderStatus?completed=true&coffeeId=1234

在 REST 情况下使用PUT方法,需要发送所有这样的数据,这将浪费带宽和内存。

PUT /coffee/orders/1234
{
 "id": 1,
 "name": "Mocha"
 "size": "Small", 
 "type": "Latte", 
 "status": "COMPLETED"
}

为了避免在进行小的更新时发送整个数据,另一个解决方案是使用PATCH进行部分更新:

PATCH /coffee/orders/1234
{
"status": "COMPLETED"
}

然而,并非所有的 Web 服务器和客户端都会提供对PATCH的支持,因此人们一直在支持使用POSTPUT进行部分更新:

POST /coffee/orders/1234
{
"status": "COMPLETED"
}

使用PUT进行部分更新:

PUT /coffee/orders/1234
{
"status": "COMPLETED"
}

总之,使用PUTPOST进行部分更新都是可以接受的。Facebook API 使用POST来更新部分资源。使用部分PUT将更符合我们实现 RESTful 资源和方法的方式,作为 CRUD 操作。

要实现对PATCH方法的支持,可以在 JAX-RS 中添加注释:

  @Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@HttpMethod("PATCH")public @interface PATCH {}

上面的片段显示了如何将javax.ws.rs.HTTPMethod的注释与名称“PATCH”相关联。一旦创建了这个注释,那么@PATCH注释就可以用于任何 JAX-RS 资源方法。

JSON Patch

JSON Patch 是 RFC 6902 的一部分。它是一个旨在允许对 JSON 文档执行操作的标准。JSON Patch 可以与HTTP PATCH方法一起使用。它对于提供 JSON 文档的部分更新非常有用。媒体类型"application/json-patch+json"用于识别此类补丁文档。

它包括以下成员:

  • op:这标识要在文档上执行的操作。可接受的值为"add""replace""move""remove""copy""test"。任何其他值都是错误的。

  • path:这是表示 JSON 文档中位置的 JSON 指针。

  • value:这表示要在 JSON 文档中替换的值。

move操作需要一个"from"成员,用于标识要从中移动值的目标文档中的位置。

这是一个 JSON Patch 文档的示例,发送在HTTP PATCH请求中:

PATCH /coffee/orders/1234 HTTP/1.1
Host: api.foo.com
Content-Length: 100
Content-Type: application/json-patch

[
  {"op":"replace", "path": "/status", "value": "COMPLETED"}
]

上述请求显示了如何使用 JSON Patch 来替换由资源coffee/orders/1234标识的咖啡订单的状态。操作,即上面片段中的"op",是"replace",它将值"COMPLETED"设置为 JSON 表示中状态对象的值。

JSON Patch 对于单页应用程序、实时协作、离线数据更改非常有用,也可以用于需要在大型文档中进行小型更新的应用程序。有关更多详细信息,请查看jsonpatchjs.com/,这是JSON Patch.(RFC 6902)JSON Pointer.(RFC 6901)的实现,采用 MIT 许可证。

推荐阅读

以下部分列出了与本章涵盖的主题相关的一些在线资源,可能对复习有用:

摘要

本章涵盖了缓存的基本概念,演示了不同的 HTTP 缓存头,如Cache-ControlExpires等。我们还看到了头部是如何工作的,以及 ETags 和Last-Modified头部如何用于条件GET请求以提高性能。我们介绍了缓存的最佳实践,RESTEasy 如何支持服务器端缓存,以及 Facebook API 如何使用 ETags。本章讨论了异步 RESTful 资源以及在使用异步 API 时的最佳实践。我们介绍了 HTTP Patch 和 JSON Patch(RFC 6902)以及部分更新。

下一章将涉及每个构建 RESTful 服务的开发人员都应该了解的高级主题,涉及常用模式和最佳实践,如速率限制、响应分页和 REST 资源的国际化。它还将涵盖其他主题,如 HATEOAS、REST 及其可扩展性。

第五章:高级设计原则

本章涵盖了每个开发人员在设计 RESTful 服务时必须了解的高级设计原则。它还提供了务实的见解,为开发人员提供足够的信息来构建具有 REST API 的复杂应用程序。

本章将涵盖以下主题:

  • 速率限制模式

  • 响应分页

  • 国际化和本地化

  • REST 的可插拔性和可扩展性

  • REST API 开发人员的其他主题

本章包含了不同的代码片段,但展示这些片段的完整示例作为书籍源代码下载包的一部分包含在内。

与之前的章节一样,我们将尝试覆盖读者所需的最低限度的细节,以便为其提供对本质上复杂的主题的扎实的一般理解,同时还提供足够的技术细节,以便读者能够轻松地立即开始工作。

速率限制模式

速率限制涉及限制客户端可以发出的请求数量。客户端可以根据其用于请求的访问令牌进行识别,如第三章中所述,安全性和可追溯性。另一种客户端可以被识别的方式是客户端的 IP 地址。

为了防止滥用服务器,API 必须实施节流或速率限制技术。基于客户端,速率限制应用程序可以决定是否允许请求通过。

服务器可以决定每个客户端的自然速率限制应该是多少,例如,每小时 500 个请求。客户端通过 API 调用向服务器发出请求。服务器检查请求计数是否在限制范围内。如果请求计数在限制范围内,则请求通过并且计数增加给客户端。如果客户端请求计数超过限制,服务器可以抛出 429 错误。

服务器可以选择包含一个Retry-After头部,指示客户端在可以发送下一个请求之前应等待多长时间。

应用程序的每个请求可以受到两种不同的节流的影响:具有访问令牌和没有访问令牌的请求。具有访问令牌的应用程序的请求配额可以与没有访问令牌的应用程序的请求配额不同。

以下是HTTP 429 Too Many Requests错误代码的详细信息。

注意

429 Too Many Requests (RFC 6585)

用户在一定时间内发送了太多请求。这是为了与速率限制方案一起使用。

429 Too Many Requests错误的响应可能包括一个Retry-After头部,指示客户端需要等待多长时间才能发出新的请求。以下是一个示例代码片段:

HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600
 <html>
       <head>
   <title>Too Many Requests</title>
   </head>
 <body>
 <h1>Too many Requests</h1>
       <p>100 requests per hour to this Web site per logged in use allowed.</p>
   </body>
   </html>

前面的 HTTP 响应示例设置了Retry-After头部为 3600 秒,以指示客户端可以稍后重试。此外,服务器可以发送一个X-RateLimit-Remaining头部,指示此客户端还有多少待处理的请求。

现在我们对速率限制有了一些想法,以及速率限制错误和Retry-AfterX-RateLimit-Remaining头部的工作原理,让我们通过 JAX-RS 编写代码。

项目的布局部分中的以下代码显示了如何在 JAX-RS 中实现一个简单的速率限制过滤器。

项目的布局

项目的目录布局遵循标准的 Maven 结构,简要解释如下表。此示例生成一个 WAR 文件,可以部署在任何符合 Java EE 7 标准的应用服务器上,如 GlassFish 4.0。

此示例演示了一个简单的咖啡店服务,客户可以查询他们下的特定订单。

源代码 描述
src/main/java 此目录包含咖啡店应用程序所需的所有源代码

CoffeeResource类是一个简单的 JAX-RS 资源,如下所示:

@Path("v1/coffees")
public class CoffeesResource {
    @GET
    @Path("{order}")
    @Produces(MediaType.APPLICATION_XML)
    @NotNull(message="Coffee does not exist for the order id requested")
    public Coffee getCoffee(@PathParam("order") int order) {
        return CoffeeService.getCoffee(order);
    }
}

项目中有一个CoffeeResource类,用于获取有关咖啡订单的详细信息。getCoffee方法返回一个包含订单详细信息的Coffee对象。

为了强制执行速率限制,我们将添加一个RateLimiter类,它是一个简单的 servlet 过滤器,如下图所示。

RateLimiter类将检查客户端的 IP 地址,并检查客户端发出的请求是否超过限制。以下图表详细描述了示例中涵盖的速率限制功能:

项目布局

前面的图表显示了客户端向api.com/foo发出GET请求。速率限制过滤器根据 IP 地址检查客户端的访问计数。由于客户端未超过速率限制,请求被转发到服务器。服务器可以返回 JSON、XML 或文本响应。

以下图表显示客户端向api.com/foo发出GET请求。速率限制过滤器根据 IP 地址检查客户端的访问计数。由于客户端超过了速率限制,请求未转发到服务器,并且速率限制器在 HTTP 响应中返回429 Too Many Requests错误代码。

项目布局

对速率限制示例的详细查看

要使用 JAX-RS 实现速率限制器,我们需要实现一个Filter类。如下代码片段所示:

@WebFilter(filterName = "RateLimiter",
        urlPatterns = {"/*"}
        )
public class RateLimiter implements Filter {
    private static final int REQ_LIMIT = 3;
    private static final int TIME_LIMIT = 600000;
    private static AccessCounter accessCounter = AccessCounter.getInstance();
}

前面的代码片段显示了javax.servlet.annotation包的WebFilter接口的实现。@WebFilter注解表示这个类是应用程序的过滤器。

@WebFilter注解必须在注解中具有至少一个urlPatternsvalue属性。

REQ_LIMIT常量代表在一段时间内可以发出的请求数量。TIME_LIMIT常量代表速率限制的时间持续时间,之后客户端可以接受新请求。

为简单起见,示例中的限制值较小。在实际场景中,限制可以是,例如,每分钟 60 个请求或每天 1,000 个请求。如果请求计数达到限制,Retry-After头将指示客户端在服务器处理下一个请求之前必须等待的时间。

为了跟踪与客户端关联的请求计数,我们创建了一个名为AccessCounter的类。以下是AccessCounter类的代码。AccessCounter类是一个带有@Singleton注解的Singleton类。它存储了一个包含 IP 地址作为键和与客户端相关的数据(称为AccessData)作为值的ConcurrentHashMap类。

@Singleton
public class AccessCounter {

    private static AccessCounter accessCounter;

    private static ConcurrentHashMap<String,AccessData> accessDetails = new ConcurrentHashMap<String, AccessData>();
}

AccessData类负责存储客户端的详细信息,例如请求的数量以及上次请求是何时。它是一个简单的普通旧 Java 对象POJO),如下代码片段所示:

public class AccessData {
    private long lastUpdated;
    private AtomicInteger count;

    public long getLastUpdated() {
        return lastUpdated;
    }

    public void setLastUpdated(long lastUpdated) {
        this.lastUpdated = lastUpdated;
    }

    public AtomicInteger getCount() {
        return count;
    }

    public void setCount(AtomicInteger count) {
        this.count = count;
    }

 …

如前面的代码片段所示,AccessData类有一个名为count的字段和一个名为lastUpdated的字段。每当新请求到达时,计数会递增,并且lastUpdated字段设置为当前时间。

RateLimiter类的doFilter()方法在以下代码片段中使用:

@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        String ipAddress = getIpAddress(httpServletRequest);
        if (accessCounter.contains(ipAddress)) {
            if (!requestLimitExceeded(ipAddress)) {
                accessCounter.increment(ipAddress);
                accessCounter.getAccessDetails(ipAddress).setLastUpdated(System.currentTimeMillis());

            } else {

                httpServletResponse.addIntHeader("Retry-After",TIME_LIMIT);
                httpServletResponse.sendError(429);

            }
        } else {
            accessCounter.add(ipAddress);

        }
        filterChain.doFilter(servletRequest, servletResponse)

    }

前面的代码显示了javax.servlet.Filter类的doFilter()方法,在RateLimiter实现中被重写。在这个方法中,首先确定客户端的 IP 地址。

如果accessCounter类包含 IP 地址,则在requestLimitExceeded()方法中将检查请求限制是否已超过。

如果速率限制已超过,则Retry-After标头将与429 Too Many Requests错误一起发送到httpServletResponse。如果同一客户端发出了新请求,并且大于TIME_LIMIT值,则计数器将重置为 0,并且可以再次处理来自客户端的请求。

以下是可以在响应中发送回客户端的速率限制标头:

  • X-RateLimit-Limit:客户端在特定时间段内可以发出的最大请求数

  • X-RateLimit-Remaining:当前速率限制窗口中剩余的请求数

本书附带了一个详细的示例。在将示例部署到应用程序服务器后,客户端可以进行多个请求以获取咖啡订单的详细信息。

为了简单起见,我们已经将速率限制设置为 3,时间限制设置为 10 分钟。以下是一个示例curl请求:

curl -i http://localhost:8080/ratelimiting/v1/coffees/1
HTTP/1.1 200 OK
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.0  Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition  4.0 
Content-Type: application/json
Date: Mon, 23 Jun 2014 23:27:34 GMT
Content-Length: 57

{
 "name":"Mocha",
 "order":1,
 "size":"Small",
 "type":"Brewed"
}

一旦超过速率限制,您将看到429错误:

curl -i http://localhost:8080/ratelimiting/v1/coffees/1
HTTP/1.1 429 CUSTOM
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.0  Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition  4.0 
Retry-After: 600000
Content-Language: 
Content-Type: text/html
Date: Mon, 23 Jun 2014 23:29:04 GMT
Content-Length: 1098

提示

此示例显示了如何构建自定义过滤器以实现速率限制。另一个选择是使用名为Repose的开源项目,它是一个可扩展且广泛的速率限制实现。 Repose 是一个开源的 HTTP 代理服务,提供速率限制、客户端认证、版本控制等功能。有关更多详细信息,请查看openrepose.org/

在下一节中,我们将讨论在使用 REST API 时必须遵循的最佳实践。

避免达到速率限制的最佳实践

以下是在使用 REST API 时避免达到速率限制的最佳实践。

缓存

在服务器端缓存 API 响应可以帮助避免达到速率限制。设置合理的到期时间间隔可确保数据库不会因查询而受到影响,并且如果资源未更改,则可以从缓存发送响应。例如,从 Twitter 获取的推文的应用程序可以缓存来自 Twitter API 的响应或使用 Twitter 流 API(在下一节中介绍)。理想情况下,API 消费者不应该每分钟进行相同的请求。这通常是一种带宽浪费,因为在大多数情况下将返回完全相同的结果。

不要在循环中发出调用

不在循环中发出调用是一个好习惯。服务器 API 应设计得尽可能详细,并通过在响应中发送尽可能多的细节来帮助客户端。这确保了消费者可以在一个 API 操作中获取一组对象,而不是在循环中获取单个对象。

记录请求

在客户端使用日志记录以查看客户端发出了多少请求是一个好习惯。观察日志将帮助客户端分析哪些是不冗余的查询,这些查询会增加速率限制并且可以被消除。

避免轮询

此外,消费者不应该轮询更改。客户端可以使用 WebHooks(en.wikipedia.org/wiki/Webhook)或推送通知(en.wikipedia.org/wiki/Push_technology)来接收通知,而不是轮询以查看内容是否已更改。有关 WebHooks 的更多详细信息将在第六章中给出,新兴标准和 REST 的未来

支持流式 API

API 开发人员可以支持流式 API。这可以帮助客户端避免达到速率限制。Twitter 提供的一组流式 API 为开发人员提供了低延迟访问 Twitter 全球推文数据流的机会。流式客户端不需要承担与轮询 REST 端点相关的开销,并且将收到指示已发生推文和其他事件的消息。

一旦应用程序建立到流式端点的连接,它们将收到推文的订阅,而不必担心轮询或 REST API 速率限制。

注意

Twitter REST API 速率限制案例研究

Twitter 每小时对未经身份验证的客户端的请求限制为 150 次。

基于 OAuth 的调用允许每小时基于请求中的访问令牌进行 350 次请求。

超出搜索 API 的速率限制的应用程序将收到 HTTP 420 响应代码。最佳做法是注意此错误条件,并遵守返回的 Retry-After 头。Retry-After 头的值是客户端应该在再次请求数据之前等待的秒数。如果客户端发送的请求超过每小时允许的数量,客户端将收到 420 Enhance Your Calm 错误。

提示

420 Enhance Your Calm (Twitter)

这不是 HTTP 标准的一部分,但在 Twitter 搜索和趋势 API 被限制时返回。应用程序最好实现429 Too Many Requests响应代码。

响应分页

REST API 被从 Web 到移动客户端的其他系统使用,因此,返回多个项目的响应应该分页,每页包含一定数量的项目。这就是所谓的响应分页。除了响应之外,最好还添加一些关于对象总数、页面总数和指向下一组结果的链接的附加元数据。消费者可以指定页面索引来查询结果以及每页的结果数。

在客户端未指定每页结果数的情况下,实施和记录每页结果数的默认设置是一种推荐做法。例如,GitHub 的 REST API 将默认页面大小设置为 30 条记录,最多为 100 条,并对客户端查询 API 的次数设置了速率限制。如果 API 有默认页面大小,那么查询字符串可以只指定页面索引。

以下部分涵盖了可以使用的不同类型的分页技术。API 开发人员可以根据其用例选择实现一个或多个这些技术。

分页类型

以下是可以使用的不同分页技术:

  • 基于偏移量的分页

  • 基于时间的分页

  • 基于游标的分页

基于偏移量的分页

基于偏移量的分页是客户端希望按页码和每页结果数指定结果的情况。例如,如果客户端想要查询所有已借阅的书籍的详细信息,或者已订购的咖啡,他们可以发送以下查询请求:

GET v1/coffees/orders?page=1&limit=50

以下表格详细说明了基于偏移量的分页将包括哪些查询参数:

查询参数 描述
page 这指定要返回的页面
limit 这指定了响应中可以包含的每页最大结果数

基于时间的分页

当客户端想要查询特定时间范围内的一组结果时,将使用基于时间的分页技术。

例如,要获取在特定时间范围内订购的咖啡列表,客户端可以发送以下查询:

GET v1/coffees/orders?since=140358321&until=143087472

以下表格详细说明了基于时间的分页将包括哪些查询参数:

查询参数 描述
until: 这是指向时间范围结束的 Unix 时间戳
since 这是指向时间范围开始的 Unix 时间戳
limit 这指定了响应中可以包含的每页最大结果数

基于游标的分页

基于游标的分页是一种技术,其中结果通过游标分隔成页面,并且可以使用响应中提供的下一个和上一个游标向前和向后导航结果。

基于游标的分页 API 避免在分页请求之间添加额外资源的情况下返回重复记录。这是因为游标参数是一个指针,指示从哪里恢复结果,用于后续调用。

Twitter 和基于游标的分页

以下是 Twitter 如何使用基于游标的分页的示例。获取拥有大量关注者的用户的 ID 的查询可以进行分页,并以以下格式返回:

{
    "ids": [
        385752029, 
        602890434, 
        ...
        333181469, 
        333165023
    ],
    "next_cursor": 1374004777531007833, 
    "next_cursor_str": "1374004777531007833", 
    "previous_cursor": 0, 
    "previous_cursor_str": "0"
}

next_cursor 值可以传递给下一个查询,以获取下一组结果:

GET https://api.twitter.com/1.1/followers/ids.json?screen_name=someone &cursor=1374004777531007833

使用 next_cursorprevious_cursor 值,可以轻松在结果集之间导航。

现在我们已经介绍了不同的分页技术,让我们详细介绍一个示例。以下示例显示了如何使用 JAX-RS 实现简单的基于偏移量的分页技术。

项目的布局

项目的目录布局遵循标准的 Maven 结构,以下表格简要解释了这一点。

所使用的示例是咖啡店服务的示例,可以查询到目前为止所有下的订单。

源代码 描述
src/main/java 此目录包含咖啡店应用程序所需的所有源代码

这是 CoffeeResource 类:

@Path("v1/coffees")
public class CoffeesResource {
    @GET
    @Path("orders")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Coffee> getCoffeeList( 
@QueryParam("page")  @DefaultValue("1") int page,
                                       @QueryParam("limit") @DefaultValue("10") int limit ) {
        return CoffeeService.getCoffeeList( page, limit);

    }
}

getCoffeeList() 方法接受两个 QueryParam 值:pagelimitpage QueryParam 值对应于页面索引,limit 对应于每页的结果数。@DefaultValue 注释指定了如果查询参数不存在可以使用的默认值。

以下是运行示例时的输出。metadata 元素包含 totalCount 值的详细信息,即记录的总数。此外,还有 JSONArraylinks 属性,其中包含诸如 self(当前页面)和 next(获取更多结果的下一个链接)等详细信息。

{
    "metadata": {
        "resultsPerPage": 10,
        "totalCount": 100,
        "links": [
            {
                "self": "/orders?page=1&limit=10"
            },
            {
                "next": "/orders?page=2&limit=10"
            }
        ]
    },
    "coffees": [
        {
            "Id": 10,
            "Name": "Expresso",
            "Price": 2.77,
            "Type": "Hot",
            "Size": "Large"
        },
        {
            "Id": 11,
            "Name": "Cappuchino",
            "Price": 0.14,
            "Type": "Brewed",
            "Size": "Large"
        },
…..
       ……
    ]
}

示例与本书可下载的源代码包捆绑在一起。

提示

在 REST API 中,为分页包含每页结果数的默认值始终是一个好习惯。此外,建议 API 开发人员在响应中添加元数据,以便 API 的消费者可以轻松获取附加信息,以获取下一组结果。

国际化和本地化

通常,服务需要在全球环境中运行,并且响应需要根据国家和语言环境进行定制。本地化参数可以在以下字段之一中指定:

  • HTTP 头

  • 查询参数

  • REST 响应的内容

语言协商类似于内容协商;HTTP 头 Accept-Language 可以根据 ISO-3166 国家代码的任何两字母首字母取不同的语言代码(www.iso.org/iso/country_codes.htm))。Content-Language 头类似于 Content-Type 头,可以指定响应的语言。

例如,以下是在客户端发送的请求的响应中发送的 Content-Language 头:

HTTP/1.1 200 OK
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.0  Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition  4.0 
Content-Language: en
Content-Type: text/html
Date: Mon, 23 Jun 2014 23:29:04 GMT
Content-Length: 1098

前面的响应将 Content-Language 设置为 en 作为响应的一部分。

JAX-RS 支持使用 javax.ws.rs.core.Variant 类和 Request 对象进行运行时内容协商。Variant 类可以包含媒体类型、语言和编码。Variant.VariantListBuilder 类用于构建表示变体的列表。

以下代码片段显示了如何创建资源表示变体的列表:

List<Variant> variantList = 
    Variant.
      .languages("en", "fr").build();

前面的代码片段调用了 VariantListBuilder 类的 build 方法,语言为 "en""fr"

查询参数可以包括特定于语言环境的信息,以便服务器可以以该语言返回信息。

以下是一个示例:

GET v1/books?locale=fr

此查询显示了一个示例,其中将在查询参数中包含区域设置以获取图书的详细信息。此外,REST 响应的内容可以包含特定于国家/地区的细节,如货币代码,以及基于请求中发送的 HTTP 标头或查询参数的其他细节。

其他主题

以下部分涵盖了一些杂项主题的细节,如 HATEOAS 和 REST 中的可扩展性。

HATEOAS

超媒体作为应用状态的引擎HATEOAS)是 REST 应用程序架构的一个约束。

超媒体驱动的 API 通过在服务器发送的响应中提供超媒体链接,提供有关可用 API 和消费者可以采取的相应操作的详细信息。

例如,包含名称和 ISBN 等数据的 REST 资源的图书表示如下所示:

{ 
   "Name":" Developing RESTful Services with JAX-RS 2.0,
            WebSockets, and JSON",
   "ISBN": "1782178120"
}

HATEOAS 实现将返回以下内容:

{
    "Name":" Developing RESTful Services with JAX-RS 2.0, 
             WebSockets, and JSON",
    "ISBN": "1782178120"
    "links": [
       {
        "rel": "self",
        "href": "http://packt.com/books/123456789"
       }
    ]
}

在前面的示例中,links元素具有relhref JSON 对象。

在这个例子中,rel属性是一个自引用的超链接。更复杂的系统可能包括其他关系。例如,图书订单可能具有"rel":"customer"关系,将图书订单链接到其客户。href是一个完整的 URL,唯一定义资源。

HATEOAS 的优势在于它帮助客户端开发人员探索协议。链接为客户端开发人员提供了关于可能的下一步操作的提示。虽然没有超媒体控件的标准,但建议遵循 ATOM RFC(4287)。

注意

根据 Richardson 成熟度模型,HATEOAS 被认为是 REST 的最终级别。这意味着每个链接都被假定实现标准的 REST 动词GETPOSTPUTDELETE。使用links元素添加详细信息,如前面代码片段所示,可以为客户端提供导航服务和采取下一步操作所需的信息。

PayPal REST API 和 HATEOAS

PayPal REST API 提供 HATEOAS 支持,因此每个响应都包含一组链接,可以帮助消费者决定下一步要采取的操作。

例如,PayPal REST API 的示例响应包括以下代码中显示的 JSON 对象:

{
    "href": "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout&token=EC-60U79048BN7719609",
    "rel": "approval_url",
    "method": "REDIRECT"
  },
  {
    "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAY-6RV70583SB702805EKEYSZ6Y/execute",
    "rel": "execute",
    "method": "POST"
  }

属性的简要描述如下。

  • href:这包含可用于未来 REST API 调用的 URL 的信息

  • rel:此链接显示它与先前的 REST API 调用相关

  • method:显示用于 REST API 调用的方法

注意

有关更多详细信息,请查看developer.paypal.com/docs/integration/direct/paypal-rest-payment-hateoas-links/

REST 和可扩展性

基于设计风格的约束的 RESTful 应用程序在时间上更具可扩展性和可维护性。基于设计风格的 RESTful 应用程序更易于理解和使用,主要是因为它们的简单性。它们也更可预测,因为一切都是关于资源。此外,与需要解析复杂 WSDL 文档才能开始理解发生了什么的 XML-RPC 应用程序相比,RESTful 应用程序更易于使用。

REST API 的其他主题

以下部分列出了对 REST 开发人员可能有用的其他主题。我们已经在早期章节中涵盖了从设计 RESTful 服务、错误处理、验证、身份验证和缓存到速率限制的主题。本节重点介绍了其他实用工具,以赋予 REST API 开发人员更好的测试和文档编制能力。

测试 RESTful 服务

拥有一组自动化测试总是有效的,可以验证服务器发送的响应。用于构建 RESTful 服务的自动化测试的一个框架是 REST Assured。

REST Assured 是用于轻松测试 RESTful 服务的 Java DSL。它支持GETPUTPOSTHEADOPTIONSPATCH,可以用于验证服务器发送的响应。

以下是一个获取咖啡订单并验证响应中返回的 ID 的示例:

    get("order").
    then().assertThat().
    body("coffee.id",equalTo(5));

在上面的片段中,我们调用获取咖啡订单并验证coffee.id值为 5。

REST Assured 支持轻松指定和验证参数、标头、Cookie 和主体,也支持将 Java 对象与 JSON 和 XML 相互映射。有关更多详细信息,您可以查看code.google.com/p/rest-assured/

记录 RESTful 服务

为消费者构建 RESTful 服务时,无论他们来自同一企业还是来自外部应用程序或移动客户端,提供文档都是一个良好的实践。以下部分涵盖了一些为 RESTful 服务提供良好文档的框架。

Swagger 是一个用于描述、生成、消费和可视化 RESTful web 服务的框架实现。方法、参数和模型的文档紧密集成到服务器代码中。Swagger 是与语言无关的,Scala、Java 和 HTML5 的实现都可用。

有关如何将 Swagger 添加到 REST API 的教程可在以下网址找到:

github.com/wordnik/swagger-core/wiki/Adding-Swagger-to-your-API

推荐阅读

以下链接涉及本章涵盖的一些主题,对于审查和获取详细信息将会很有用:

摘要

本章涵盖了每个 RESTful API 开发人员都应该了解的高级主题。一开始,我们看到了速率限制示例,演示了如何强制执行节流,以便服务器不会被 API 调用淹没。我们还看到了 Twitter、GitHub 和 Facebook API 如何执行速率限制。我们涵盖了不同的分页技术和基本分页示例以及最佳实践。然后,我们转向国际化和其他杂项主题。最后,我们涵盖了 HATEOAS 以及它如何成为 REST API、REST 和可扩展性主题的下一个级别。

下一章将涵盖其他新兴标准,如 WebSockets、WebHooks 以及 REST 在不断发展的 Web 标准中的作用。

第六章:新兴标准和 REST 的未来

本章涵盖了新兴和发展中的技术,将增强 RESTful 服务的功能,并提供对 REST 的未来以及其他实时 API 支持者的一些看法。我们将涵盖一些实时 API,并看看它们如何帮助解决轮询等旧方式的问题。鉴于 Twitter、Facebook 和 Stripe 等平台的普遍流行,它们采用了一种范式转变,因此提供了实时 API,以在事件发生时向客户端提供信息,这并不奇怪。

本章将涵盖以下主题:

  • 实时 API

  • 轮询

  • WebHooks

  • WebSockets

  • 额外的实时 API 支持者,包括以下内容:

  • PubSubHubbub

  • 服务器发送事件

  • XMPP

  • XMPP 上的 BOSH

  • 使用 WebHooks 和 WebSockets 的公司案例

  • WebHooks 和 WebSockets 的比较

  • REST 和微服务

我们将从定义实时 API 的含义开始,然后讨论轮询及其缺点。接下来,我们将详细介绍广泛用于异步实时通信的不同模型。最后,我们将详细阐述 WebHooks 和 WebSockets 的务实方法。

实时 API

在我们的情境中,实时 API 帮助 API 消费者在事件发生时接收他们感兴趣的事件。实时更新的一个例子是当有人在 Facebook 上发布链接,或者你在 Twitter 上关注的人发表关于某个话题的推文。另一个实时 API 的例子是在股价变化发生时接收股价变化的信息。

轮询

轮询是从产生事件和更新流的数据源获取数据的最传统方式。客户端定期发出请求,如果有响应,服务器就会发送数据。如果服务器没有要发送的数据,就会返回空响应。以下图表显示了连续轮询的工作原理:

轮询

轮询带来了诸多缺点,比如在服务器没有更新时对请求返回空响应,这导致了带宽和处理时间的浪费。低频率的轮询会导致客户端错过接近更新发生时间的更新,而过于频繁的轮询也会导致资源浪费,同时还会面临服务器施加的速率限制。

为了消除轮询的这些缺点,我们将涵盖以下主题:

  • PuSH 模型-PubSubHubbub

  • 流模型

PuSH 模型-PubSubHubbub

PuSH 是基于发布/订阅协议的简单主题,基于 ATOM/RSS。它的目标是将原子源转换为实时数据,并消除影响源的消费者的轮询。订阅者在主题上注册他们的兴趣,原始发布者告诉感兴趣的订阅者有新的内容。

为了分发发布和内容分发的任务,有一个Hub的概念,可以委托发送内容给订阅者。以下图表描述了 PubSubHubbub 模型:

PuSH 模型-PubSubHubbub

让我们看看这个模型是如何工作的:

  1. Subscriber通过从Publisher获取 feed 来发现Hub

  2. 一旦Hub被发现,Subscriber就会订阅Hub感兴趣的 feed URI。

  3. 现在,当Publisher有更新要发送时,它会让Hub获取更新。

  4. Hub然后将更新发送给所有发布者。

这种模型的优势在于,发布者不必担心向所有订阅者发送更新。另一方面,订阅者有一个优势,即他们可以在事件发生时从 hub 获取更新,而无需不断地轮询发布者。

在接下来的章节中讨论的WebHooks范例使用了这个协议。

流模型

异步通信的流模型涉及保持通道打开并在数据发生时发送数据。在这种情况下,需要保持套接字连接打开。

服务器发送事件

服务器发送事件SSE)是基于流模型的技术,其中浏览器通过 HTTP 连接自动从服务器获取更新。W3C 已将服务器发送事件 EventSource API 作为 HTML5 的一部分进行了标准化。

使用 SSE,客户端使用"text/eventstream" MimeType 向服务器发起请求。一旦进行了初始握手,服务器可以在事件发生时不断向客户端发送事件。这些事件是从服务器发送到客户端的纯文本消息。它们可以是客户端侧的事件监听器可以消耗的数据,事件监听器可以解释并对接收到的事件做出反应。

SSE 定义了从服务器发送到客户端的事件的消息格式。消息格式由一系列以换行符分隔的纯文本行组成。携带消息主体或数据的行以data:开头,以\n\n结尾,如下面的代码片段所示:

data: My message \n\n

携带一些服务质量QoS)指令的行(例如retryid)以 QoS 属性名称开头,后跟:,然后是 QoS 属性的值。标准格式使得可以开发围绕 SSE 的通用库,以使软件开发更加容易。

以下图表显示了 SSE 的工作原理:

服务器发送事件

如图所示,客户端订阅了一个事件源。服务器会在事件发生时不断发送更新。

此外,服务器可以将 ID 与整个消息关联并发送,如下面的代码片段所示:

id: 12345\n
data: Message1\n
data: Message 2\n\n

前面的代码片段显示了如何发送带有事件 ID 和数据的多行消息,最后一行以两个\n\n字符结尾。

设置一个 ID 让客户端能够跟踪最后触发的事件,这样如果与服务器的连接断开,客户端发送的新请求中会设置一个特殊的 HTTP 头(Last-Event-ID)。

接下来的部分将介绍如何将 ID 与 SSE 关联,SSE 在连接丢失和重试时的工作原理,以及如何将事件名称与 SSE 关联。

将 ID 与事件关联

每个 SSE 消息都可以有一个消息标识符,可以用于各种目的,例如跟踪客户端接收到的消息,并为其保留一个检查点。当消息 ID 在 SSE 中使用时,客户端可以将最后的消息 ID 作为连接参数之一提供,以指示服务器从特定消息开始恢复。当然,服务器端代码应该实现一个适当的过程,以从客户端请求的消息 ID 恢复通信。

以下代码片段显示了带有 ID 的 SSE 消息的示例:

id: 123 \n
data: This is a single line event \n\n

在连接失败的情况下重试

Firefox、Chrome、Opera 和 Safari 支持服务器发送事件。如果浏览器和服务器之间出现连接丢失,浏览器可以尝试重新连接到服务器。服务器可以配置一个重试指令,以允许客户端进行重试。重试间隔的默认值为 3 秒。服务器可以发送一个重试事件来增加重试间隔到 5 秒,如下所示:

retry: 5000\n
data: This is a single line data\n\n

将事件名称与事件关联

另一个 SSE 指令是事件名称。每个事件源可以生成多种类型的事件,客户端可以根据订阅的事件类型决定如何消费每种事件类型。以下代码片段显示了name事件指令如何融入消息中:

event: bookavailable\n
data: {"name" : "Game of Thrones"}\n\n
event: newbookadded\n
data: {"name" :"Storm of Swords"}\n\n

服务器发送事件和 JavaScript

被认为是 JavaScript 开发人员在客户端中 SSE 的基础 API 是EventSource接口。EventSource接口包含相当多的函数和属性,但最重要的函数列在下表中:

函数名 描述
addEventListener 此函数添加事件监听器,以处理基于事件类型的传入事件。
removeEventListener 此函数移除已注册的监听器。
onmessage 当消息到达时调用此函数。使用onmessage方法时,没有自定义事件处理可用。监听器管理自定义事件处理。
onerror 当连接出现问题时调用此函数。
onopen 当连接打开时调用此函数。
onclose 当连接关闭时调用此函数。

以下代码片段显示了如何订阅一个来源省略的不同事件类型。代码片段假定传入的消息是 JSON 格式的消息。例如,有一个应用程序可以在某个存储中有新书可用时向用户流式传输更新。'bookavailable'监听器使用简单的 JSON 解析器来解析传入的 JSON。

然后,它将用此来更新 GUI,而'newbookadded'监听器使用恢复函数来过滤并选择性处理 JSON 对。

var source = new EventSource('books');
source.addEventListener('bookavailable', function(e) {
  var data = JSON.parse(e.data);
  // use data to update some GUI element...
}, false);

source.addEventListener('newbookadded', function(e) {
  var data = JSON.parse(e.data, function (key, value) {
    var type;
    if (value && typeof value === 'string') {
return "String value is: "+value;
    }
    return value;

服务器发送事件和 Jersey

SSE 不是标准 JAX-RS 规范的一部分。然而,在 JAX-RS 的 Jersey 实现中支持它们。更多细节请查看jersey.java.net/documentation/latest/sse.html

WebHooks

WebHooks是一种用户定义的自定义 HTTP 回调形式。在 WebHook 模型中,客户端提供事件生成器的端点,事件生成器可以向其发布事件。当事件发布到端点时,对此类事件感兴趣的客户端应用程序可以采取适当的操作。WebHooks 的一个例子是使用 GIT post-receive hook 触发 Hudson 作业等事件。

为了确认订阅者正常接收到 WebHook,订阅者的端点应返回200 OK HTTP状态码。事件生成器将忽略请求正文和除状态外的任何其他请求标头。任何 200 范围之外的响应代码,包括 3xx 代码,都将表示他们未收到 WebHook,并且 API 可能会重试发送 HTTP POST请求。

GitHub 生成的 WebHooks 事件传递了有关存储库中活动的信息负载。WebHooks 可以触发多种不同的操作。例如,消费者可能在进行提交时、复制存储库时或创建问题时请求信息负载。

以下图表描述了 WebHooks 如何与 GitHub 或 GitLab 一起工作:

WebHooks

让我们看看 WebHooks 是如何工作的:

  1. 用户进行Git推送。

  2. 消费者与 GitHub 注册的事件对象有一个自定义的 WebHook URL。例如,当发生事件时,比如进行提交时,GitHub 服务将使用POST消息将有关提交的信息负载发送到消费者提供的端点。

  3. 然后,消费应用程序可以将数据存储在dB中,或者执行其他操作,比如触发持续集成构建。

注意

一些流行的 WebHooks 案例研究

Twilio 使用 WebHooks 发送短信。GitHub 使用 WebHooks 发送存储库更改通知,以及可选的一些负载。

PayPal 使用即时付款通知IPN),这是一种自动通知商家与 PayPal 交易相关事件的消息服务,它基于 WebHooks。

Facebook 的实时 API 使用 WebHooks,并基于PubSubHubbubPuSH)。

如前所述,如果一个 API 没有提供 WebHooks 形式的通知,其消费者将不得不不断轮询数据,这不仅效率低下,而且不是实时的。

WebSockets

WebSocket 协议是一种在单个 TCP 连接上提供全双工通信通道的协议。

WebSocket 协议是一种独立的基于 TCP 的协议,它与 HTTP 的唯一关系是,切换到 WebSockets 的握手被 HTTP 服务器解释为Upgrade请求。

它提供了在客户端(例如 Web 浏览器)和端点之间进行全双工、实时通信的选项,而无需不断建立连接或密集轮询资源。WebSockets 广泛用于社交动态、多人游戏、协作编辑等领域。

以下行显示了 WebSocket 协议握手的示例,从Upgrade请求开始:

GET /text HTTP/1.1\r\n Upgrade: WebSocket\r\n Connection: Upgrade\r\n Host: www.websocket.org\r\n …\r\n 
HTTP/1.1 101 WebSocket Protocol Handshake\r\n 
Upgrade: WebSocket\r\n 
Connection: Upgrade\r\n 
…\r\n

下图显示了一个握手的示例,使用了HTTP/1.1 Upgrade请求和HTTP/1.1 Switching Protocols响应:

WebSockets

一旦客户端和服务器之间建立了连接,使用Upgrade请求和HTTP/1.1响应,WebSocket 数据帧(二进制或文本)可以在客户端和服务器之间双向发送。

WebSockets 数据最小帧为 2 字节;与 HTTP 头部传输相比,这大大减少了开销。

下面是使用 JavaScript WebSockets API 的一个非常基本的示例:

//Constructionof the WebSocket object
var websocket = new WebSocket("coffee"); 
//Setting the message event Function
websocket.onmessage = function(evt) { 
onMessageFunc(evt)
};
//onMessageFunc which when a message arrives is invoked.
function onMessageFunc (evt) { 
//Perform some GUI update depending the message content
}
//Sending a message to the server
websocket.send("coffee.selected.id=1020"); 
//Setting an event listener for the event type "open".
addEventListener('open', function(e){
        onOpenFunc(evt)});

//Close the connection.
websocket.close();

以下表格将详细描述 WebSockets 功能和各种函数:

函数名 描述
send 这个函数可以用来向服务器指定的 URL 发送消息。
onopen 当连接创建时,将调用此函数。onopen函数处理open事件类型。
onmessage 当新消息到达时,将调用onmessage函数来处理message事件。
onclose 当连接被关闭时,将调用此函数。onclose方法处理close事件类型。
onerror 当通信通道发生错误时,将调用此函数来处理error事件。
close 这个函数用于关闭通信套接字并结束客户端和服务器之间的交互。

注意

流行的 WebSockets 案例研究

德州扑克是最早大规模利用 WebSockets 连接的游戏之一。在德州扑克 HTML5 中使用 WebSockets 可以提供流畅、高速的游戏体验,允许在移动网络上实现同步体验。根据连接的不同,游戏加载和刷新几乎立即完成。

额外的实时 API 支持者

还有一些常用的实时或几乎实时通信协议和 API,它们大多数在浏览器之外使用。其中一些协议和 API 将在接下来的部分中描述。

XMPP

XMPP 协议是为满足文本消息和互联网聊天导向解决方案的要求而开发的。XMPP 的基本通信模型是客户端到服务器、服务器到服务器、服务器到客户端。为了支持这一点,它定义了基于 XML 消息的客户端到服务器协议和服务器到服务器协议,直接通过 TCP 编码和传输。

XMPP 是一种成熟的协议,在不同语言和平台上有许多实现。与 XMPP 相关的主要缺点是长轮询和开放套接字来处理入站和出站通信。

XMPP 上的 BOSH

同步 HTTP 上的双向流BOSH)在 XEP-0124 中规定了在 HTTP 上进行 XMPP 的标准方式。对于客户端发起的协议,客户端简单地在 HTTP 上发送 XMPP 数据包,对于服务器发起的协议,服务器使用长轮询,连接在预定的时间内保持打开状态。

BOSH 的主要优势在于它提供了使用 Web 浏览器作为 XMPP 客户端的可能性,利用了 BOSH 的任何 JavaScript 实现。Emite、JSJaC 和 xmpp4js 是一些支持 BOSH 的库。

WebHooks、WebSockets 和服务器发送事件之间的比较

与 WebSockets 不同,SSE 是通过 HTTP 发送的。SSE 仅提供了从服务器到客户端的事件单向通信,并不像 WebSockets 那样支持全双工通信。SSE 具有自动重试连接的能力;它们还具有可以与消息关联的事件 ID,以提供服务质量QoS)功能。WebSockets 规范不支持这些功能。

另一方面,WebSockets 支持全双工通信,减少了延迟并有助于提高吞吐量,因为它们在 HTTP 上进行了初始握手,然后消息在端点之间通过 TCP 传输。

与前面提到的两种协议相比,WebHooks 的准入门槛较低,并为应用程序和服务提供了一种简单的集成方式。这使得能够通过 HTTP 请求使一组松散耦合的云服务相互连接和交换。

下表比较和对比了 WebHooks、WebSockets 和 SSE 在不同领域的情况:

标准 WebHooks WebSockets 服务器发送事件
异步实时通信支持
回调 URL 注册
长期开放连接
双向
错误处理
易于支持和实现 需要浏览器和代理服务器支持
需要回退到轮询

接下来的部分将介绍高可用云应用程序如何向基于微服务的架构迈进。

REST 和微服务

随着微服务架构的出现,SOA 的梦想已经成为现实,微服务架构将单片应用程序分解为一组细粒度服务。我们现在将看一下微服务相对于单片服务的不同优势。

简单性

许多开发人员发现,与使用更复杂的传统企业相比,使用轻量级 API 服务构建相同的应用程序更具弹性、可扩展性和可维护性。这种风格就是基于微服务的架构。这与诸如 CORBA 和 RMI 的传统 RPC 方法或 SOAP 等庞大的 Web 服务协议的方法形成对比。

问题的隔离

在单片应用程序中,服务的所有组件都加载在单个应用程序构件(WAR、EAR 或 JAR 文件)中,该构件部署在单个 JVM 上。这意味着如果应用程序或应用程序服务器崩溃,将导致所有服务的失败。

然而,使用微服务架构,服务可以是独立的 WAR/EAR 文件。服务可以通过 REST 和 JSON 或 XML 相互通信。在微服务架构中,另一种服务之间通信的方式是使用 AMQP/Rabbit MQ 等消息协议。

扩展和缩减

对于单片服务,部署的应用程序文件中并非所有服务都需要进行扩展,但它们都被迫遵循在部署级别制定的相同扩展和缩减规则。

使用微服务架构,可以通过较小的服务构建应用程序,这些服务可以独立部署和扩展。这导致了一种对故障具有弹性、可扩展和灵活的架构,可以从特性定义阶段快速开发、构建和部署服务,直到生产阶段。

能力的清晰分离

在微服务架构中,这些服务可以根据业务能力进行组织。例如,库存服务可以与计费服务分开,而计费服务可以与运输服务分开。如果其中一个服务失败,其他服务仍然可以继续提供请求,正如问题隔离部分所述。

语言独立性

微服务架构的另一个优势是,这些服务是使用简单易用的 REST/JSON API 构建的,可以轻松被其他语言或框架(如 PHP、Ruby-On-Rails、Python 和 node.js)消费。

亚马逊和 Netflix 是微服务架构的先驱之一。eBay 开源了 Turmeric,这是一个全面的、基于策略驱动的 SOA 平台,可用于开发、部署、保护、运行和监控 SOA 服务和消费者。

推荐阅读

以下是一些额外资源的链接,感兴趣的读者可以查看,以更全面地了解本章提到的用例:

总结

在本章中,我们涵盖了 WebHooks、SSEs、WebSockets 等高级主题,以及它们在本章中的使用场景和方式。本章的主要收获之一是要理解提供实时 API 的重要性,以避免与重复轮询相关的低效。我们看到了一些公司在其解决方案中同时使用 WebHooks 和 WebSockets 的案例研究。我们在整本书的各个章节中看到了不同的最佳实践和设计原则;作为总结,本章对 REST 和异步通信的未来提供了实质性的介绍。社交数据的大量增加有可能成为发展语义网络的重要推动力,这将使代理能够代表我们执行非平凡的操作,并使用我们讨论过的各种模式进行实时更新。

此外,我们看到高可用云应用程序往往会转向网络化组件模型,应用程序会被分解为可以使用微服务架构独立部署和扩展的服务。要了解更多关于构建 RESTful 服务的详细信息,请查看书籍Developing RESTful Services with JAX-RS2.0, WebSockets, and JSON,作者 Bhakti Mehta 和 Masoud Kalali,出版社 Packt Publishing。

附录 A. 附录

在这个社交网络、云计算和移动应用的时代,人们希望与他人保持联系,发表意见,协作构建应用程序,分享输入并提出问题。从www.statisticbrain.com/twitter-statistics/中提到的数据可以看出,Twitter 拥有大约 650 万用户,每天有 5800 万条推文。同样,Facebook 的统计数据也令人震惊:13 亿用户使其成为社交网络平台的核心。多年来,GitHub 已经发展成为默认的社交编码平台。因此,Twitter、Facebook 和 GitHub 是最广泛使用的构建应用程序、挖掘数据以及构建与分析相关信息的平台之一。

前几章涵盖了构建 RESTful 服务、添加性能、缓存、安全性以及 RESTful 服务的扩展等主题,本章将重点介绍一些流行的 REST 平台以及它们如何与之前章节中涵盖的不同模式相结合,作为它们的 API 基础设施的一部分。

本章将涵盖以下主题:

  • GitHub REST API 概述

  • Facebook Open Graph API 概述

  • Twitter REST API 概述

GitHub REST API 概述

GitHub 已经成为极其流行的社交协作编码平台,用于构建代码以及为其他存储库做出贡献。开发人员使用它来创建、构建和部署软件,使用范围从个人项目到各种企业使用它作为其流程的一部分。GitHub 在其服务的 API 文档中有详尽的文档,网址为developer.github.com/v3/

以下部分详细介绍了 GitHub 如何处理我们在之前章节中涵盖的所有不同模式。

从 GitHub 获取详细信息

以下命令显示了如何使用未经身份验证的 cURL 命令来获取用户的数据,获取存储库的详细信息等。

以下命令获取javaee-samples用户的详细信息:

curl https://api.github.com/users/javaee-samples
{
 "login": "javaee-samples",
 "id": 6052086,
 "avatar_url": "https://avatars.githubusercontent.com/u/6052086?",
 "gravatar_id": null,
 "url": "https://api.github.com/users/javaee-samples",
 "html_url": "https://github.com/javaee-samples",
 "followers_url": "https://api.github.com/users/javaee-samples/followers",
 "following_url": "https://api.github.com/users/javaee-samples/following{/other_user}",
 "gists_url": "https://api.github.com/users/javaee-samples/gists{/gist_id}",
 "starred_url": "https://api.github.com/users/javaee-samples/starred{/owner}{/repo}",
 "subscriptions_url": "https://api.github.com/users/javaee-samples/subscriptions",
 "organizations_url": "https://api.github.com/users/javaee-samples/orgs",
 "repos_url": "https://api.github.com/users/javaee-samples/repos",
 "events_url": "https://api.github.com/users/javaee-samples/events{/privacy}",
 "received_events_url": "https://api.github.com/users/javaee-samples/received_events",
 "type": "Organization",
 "site_admin": false,
 "name": "JavaEE Samples",
 "company": null,
 "blog": "https://arungupta.ci.cloudbees.com/",
 "location": null,
 "email": null,
 "hireable": false,
 "bio": null,
 "public_repos": 11,
 "public_gists": 0,
 "followers": 0,
 "following": 0,
 "created_at": "2013-11-27T17:17:00Z",
 "updated_at": "2014-07-03T16:17:51Z"

注意

如前述命令所示,前述响应中有不同的 URL,可用于获取关注者、提交等详细信息。这种呈现 URL 的方式与我们在本书早期使用linkshrefrel等方式介绍的 HATEOAS 示例不同。这显示了不同平台选择不同方式提供连接服务的方式,这是不言自明的。

要获取用户的存储库并进行分页,可以使用如下查询:

curl https://api.github.com/users/javaee-samples/repos?page=1&per_page=10
…..

GitHub API 使用 OAuth2 来对用户进行身份验证。所有使用 GitHub API 的开发人员都需要注册他们的应用程序。注册的应用程序会被分配一个唯一的客户端 ID 和客户端密钥。

有关为用户获取经过身份验证的请求的更多详细信息,请查看developer.github.com/v3/oauth/

动词和资源操作

以下表格涵盖了 GitHub API 如何使用动词来执行特定资源的操作:

动词 描述
HEAD 用于获取 HTTP 头信息
GET 用于检索资源,比如用户详细信息
POST 用于创建资源,比如合并拉取请求
PATCH 用于对资源进行部分更新
PUT 用于替换资源,比如更新用户
DELETE 用于删除资源,比如将用户移除为协作者

版本控制

GitHub API 在其 URI 中使用版本 v3。API 的默认版本可能会在将来更改。如果客户端依赖于特定版本,他们建议明确发送一个Accept头,如下所示:

Accept: application/vnd.github.v3+json

错误处理

如第二章中所述,资源设计,客户端错误由400 错误代码表示。GitHub 使用类似的约定来表示错误。

如果使用 API 的客户端发送无效的 JSON,则会返回400 Bad Request响应给客户端。如果使用 API 的客户端在请求体中漏掉了字段,则会返回422 Unprocessable Entity响应给客户端。

速率限制

GitHub API 还支持速率限制,以防止服务器因某些恶意客户端的过多请求而导致失败。对于使用基本身份验证OAuth的请求,客户端每小时最多可以发出 5,000 个请求。对于未经身份验证的请求,客户端每小时的速率限制为 60 个请求。GitHub 使用X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset头来告知速率限制的状态。

因此,我们已经介绍了 GitHub API 的细节,介绍了他们选择如何实现本书中迄今为止介绍的一些 REST 原则。下一节将介绍 Facebook Open Graph REST API,涵盖版本控制、错误处理、速率限制等主题。

Facebook Graph API 概述

Facebook Graph API 是从 Facebook 数据中获取信息的一种方式。使用 HTTP REST API,客户端可以执行各种任务,如查询数据、发布更新和图片、获取相册和创建相册、获取节点的点赞数、获取评论等。下一节将介绍如何访问 Facebook Graph API。

注意

在 Web 上,Facebook 使用 OAuth 2.0 协议的变体进行身份验证和授权。原生的 Facebook 应用程序用于 iOS 和 Android。

要使用 Facebook API,客户端需要获取一个访问令牌来使用 OAuth 2.0。以下步骤显示了如何创建应用程序 ID 和密钥,然后获取访问令牌来执行对 Facebook 数据的查询:

  1. 前往developers.facebook.com/apps。您可以创建一个新的应用程序。创建应用程序后,您将被分配应用程序 ID 和密钥,如下面的屏幕截图所示:Facebook Graph API 概述

  2. 一旦您获得了应用程序 ID 和密钥,就可以获取访问令牌并执行对 Facebook 数据的查询。

注意

Facebook 有一个特殊的/me端点,对应于正在使用访问令牌的用户。要获取用户的照片,请求可以如下所示:

GET /graph.facebook.com/me/photos

  1. 要发布消息,用户可以调用如下简单的 API:
      POST /graph.facebook.com/me/feed?message="foo"
       &access_token="…."
  1. 要使用 Graph Explorer 获取您的 ID、名称和照片的详细信息,查询如下:
https://developers.facebook.com/tools/explorer?method=GET&path=me%3Ffields=id,name
  1. 下面的屏幕截图显示了一个 Graph API Explorer 查询,节点为dalailama。点击 ID 可以查看节点的更多详细信息。Facebook Graph API 概述

因此,我们看到如何使用 Graph API Explorer 应用程序来构建社交图中节点的查询。我们可以通过各种字段(如 ID 和名称)进行查询,并尝试使用GETPOSTDELETE等方法。

动词和资源操作

下表总结了 Facebook Graph API 中常用的动词:

动词 描述
GET 用于检索资源,如动态、相册、帖子等
POST 用于创建资源,如动态、帖子、相册等
PUT 用于替换资源
DELETE 用于删除资源

提示

一个重要的观察是,Facebook Graph API 使用POST而不是PUT来更新资源。

版本控制

Graph API 目前使用的是 2014 年 8 月 7 日发布的 2.1 版本。客户端可以在请求 URL 中指定版本。如果客户端没有指定版本,Facebook Open Graph API 将默认使用最新可用的版本。每个版本保证在 2 年内可用,之后如果客户端使用旧版本进行任何调用,它们将被重定向到 API 的最新版本。

错误处理

以下片段显示了失败的 API 请求的错误响应:

    {
       "error": {
         "message": "Message describing the error",
         "type": "OAuthException",
         "code": 190 ,
        "error_subcode": 460
       }
     }

如前面的代码所示,错误消息中有称为codeerror_subcode的 JSON 对象,可用于找出问题所在以及恢复操作。在这种情况下,code的值是190,这是一个OAuthException值,而error_subcode值为460,表示密码可能已更改,因此access_token无效。

速率限制

Facebook Graph API 根据使用 API 的实体是用户、应用程序还是广告,具有不同的速率限制政策。当用户的调用超过限制时,用户将被阻止 30 分钟。有关更多详细信息,请查看developers.facebook.com/docs/reference/ads-api/api-rate-limiting/。下一节将介绍 Twitter REST API 的详细信息。

Twitter API 概述

Twitter API 具有 REST API 和 Streaming API,允许开发人员访问核心数据,如时间线、状态数据、用户信息等。

Twitter 使用三步 OAuth 进行请求。

注意

Twitter API 中 OAuth 的重要方面

客户端应用程序不需要存储登录 ID 和密码。应用程序发送代表用户的访问令牌,而不是使用用户凭据的每个请求。

为了成功完成请求,POST变量、查询参数和请求的 URL 始终保持不变。

用户决定哪些应用程序可以代表他,并随时可以取消授权。

每个请求的唯一标识符(oauth_nonce标识符)防止重放相同的请求,以防它被窥探。

对于向 Twitter 发送请求,大多数开发人员可能会发现初始设置有点令人困惑。blog.twitter.com/2011/improved-oauth-10a-experience的文章显示了如何创建应用程序、生成密钥以及使用 OAuth 工具生成请求。

以下是 Twitter 中 OAuth 工具生成的请求示例,显示了获取twitterapi句柄状态的查询:

注意

Twitter API 不支持未经身份验证的请求,并且具有非常严格的速率限制政策。

curl --get 'https://api.twitter.com/1.1/statuses/user_timeline.json' --data 'screen_name=twitterapi' --header 'Authorization: OAuth oauth_consumer_key="w2444553d23cWKnuxrlvnsjWWQ", oauth_nonce="dhg2222324b268a887cdd900009ge4a7346", oauth_signature="Dqwe2jru1NWgdFIKm9cOvQhghmdP4c%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1404519549", oauth_token="456356j901-A880LMupyw4iCnVAm24t33HmnuGOCuNzABhg5QJ3SN8Y", oauth_version="1.0"'—verbose.

这会产生如下输出:

GET /1.1/statuses/user_timeline.json?screen_name=twitterapi HTTP/1.1
Host: api.twitter.com
Accept: */*
 HTTP/1.1 200 OK
…
"url":"http:\/\/t.co\/78pYTvWfJd","entities":{"url":{"urls":[{"url":"http:\/\/t.co\/78pYTvWfJd","expanded_url":"http:\/\/dev.twitter.com","display_url":"dev.twitter.com","indices":[0,22]}]},"description":{"urls":[]}},"protected":false,"followers_count":2224114,"friends_count":48,"listed_count":12772,"created_at":"Wed May 23 06:01:13 +0000 2007","favourites_count":26,"utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","geo_enabled":true,"verified":true,"statuses_count":3511,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/656927849\/miyt9dpjz77sc0w3d4vj….

动词和资源操作

以下表格总结了 Twitter REST API 中常用的动词:

动词 描述
GET 用于检索资源,如用户、关注者、收藏夹、订阅者等。
POST 用于创建资源,如用户、关注者、收藏夹、订阅者等。
POST与动词update 用于替换资源。例如,要更新友谊关系,URL 将是POST friendships/update
POST与动词destroy 用于删除资源,如删除直接消息、取消关注某人等。例如,URL 将是POST direct_messages/destroy

版本控制

Twitter API 的当前版本是 1.1。它仅支持 JSON,不再支持 XML、RSS 或 Atom。使用 Twitter API 版本 1.1,所有客户端都需要使用 OAuth 进行身份验证以进行查询。Twitter API 版本 1.0 已被弃用,有 6 个月的时间窗口来迁移到新版本。

错误处理

Twitter API 在对 REST API 的响应中返回标准的 HTTP 错误代码。成功时返回200 OK。当没有数据返回时返回304 Not Modified,当认证凭据丢失或不正确时返回401 Not Authorized,当出现故障并需要发布到论坛时返回500 Internal Server Error等等。除了详细的错误消息,Twitter API 还生成可机器读取的错误代码。例如,响应中的错误代码32意味着服务器无法对用户进行身份验证。更多详情,请查看dev.twitter.com/docs/error-codes-responses

推荐阅读

以下部分提供了一些链接,可能对您有所帮助:

摘要

本附录是一份由流行平台(如 GitHub、Facebook 和 Twitter)实施的 API 的简要集合,以及它们处理各种 REST 模式的方法。尽管用户可以通过 REST API 的数据做出多种可能性,但这些框架之间的共同点是使用 REST 和 JSON。这些平台的 REST API 由 Web 和移动客户端使用。本附录涵盖了这些平台如何处理版本控制、动词、错误处理,以及基于 OAuth 2.0 对请求进行认证和授权。

本书从 REST 的基础知识和如何构建自己的 RESTful 服务开始。从那时起,我们涵盖了各种主题以及构建可扩展和高性能的 REST 服务的技巧和最佳实践。我们还参考了各种库和工具,以改进 REST 服务的测试和文档,以及实时 API 的新兴标准。我们还涵盖了使用 WebSockets、WebHooks 以及 REST 的未来的案例研究。

我们希望我们的这一努力能帮助您更好地理解、学习、设计和开发未来的 REST API。

posted @ 2024-05-24 10:54  绝不原创的飞龙  阅读(48)  评论(0编辑  收藏  举报