描述 RESTful 应用程序

阅读数:5999 2009 年 2 月 4 日 01:17

Roy Fielding 最近这样说道

REST API 不应该定义固定的资源名或者层次(这是客户端和服务器间明显的耦合)。服务器必须拥有控制自己名字空间的自由。

要抓住这类陈述的要领不是件简单的事。如果服务器不将它自己的名字空间控制在一个固定的资源层次下,客户端及更重要的客户端开发者将如何知道或发现资源的 URI 呢?毕竟,长久以来,分布式客户端 / 服务器开发的一个基本假设就是:为了构建、维护和管理这类应用, 我们需要预先对应用的接口正式描述。Roy Fielding 的观点似乎跟这个假设相冲突。

关于描述 RESTful 系统的讨论并非新鲜事物。这类讨论几乎总会得出类似上述的观点。例如,看看前年 infoQ 上关于争论:REST 需要描述语言么?的备忘录,它总结了当时发生的部分讨论。今天的事态并没有什么特别的不同。

针对 RESTful 应用的正式描述语言,虽然有大量的赞成和反对意见,但像 WADL 这样的描述语言只得到了有限的发展。然而,由于缺乏一种机器能够解释的“标准”语言,服务器应用所采取的最常用方法就是记录所有 URI、支持的 HTTP 方法和表示(representation)的结构(如,对应的 XML 和 JSON 格式),这样客户端应用开发者就能依赖这种文档来编写代码。

但是,这种方式跟 REST 的一些基本原则(如 Roy Fielding 在上面所说的)有冲突。即便我们无视这一异议,对于那些试图通过 HTTP RESTful 构建分布式应用的人来说,基本问题仍然存在。不正式地定义契约,服务器怎么可能得以脱身?没有契约,我们如何能确定正确实现了客户端和服务器——不仅正确实现了各自的设计规范,而且恰当地实现了其他业务 / 技术策略?

用 HTTP 作为应用协议、以 RESTful 方式构建的分布式应用其实有一个契约,但其性质和种类却不相同。我们需要知道寻找的目标和位置。如果我们打算提出一种描述语言,那么它就要和 Roy Fielding 所说的保持一致,它不能是类似 WSDL 或 WADL 这样的东西。在这篇文章中,我的目标是回答如下问题:

  • 为什么还没有一个针对 RESTful 应用的标准描述语言?
  • RESTful 应用的契约应该是个什么样子?
  • 我们需要构建哪种软件,它才能理解和利用这样的契约?
  • 如果我们决定提出机器可读的描述,它会是什么样子?

请让我从一个示例开始。

示例

任务是写一个客户端程序,实现同一位客户在不同银行账户间的转账业务。

首先让我描述一下客户端和服务器之间的所有交互,接着看看这个契约的可能描述。

步骤 0:用户登录客户端。为了保持此次讨论的重点,请让我忽视所有安全方面的内容。

步骤 1:客户端使用 URI:http://bank.org/accounts?findby=7t676323a通过用户 ID 查找两个账户。这里的7t676323a是向银行注册了多个账户的某用户的 ID。在响应中,服务器返回两个账户 ID,即AZA12093ADK31242,各自的用户 ID 和当前余额如下:

200 OK
Content-Type: application/xml;charset=UTF-8

 AZA12093
        7t676323a
        993.95
    
     ADK31242
        7t676323a
        534.62
    

我们假设跟名字空间urn:org:bank:accounts绑定的 XML 模式描述了示例中用到的 XML 文档。

步骤 2:由于客户端知道两个账户的 ID,在必要情况下,它可以向以下 URI 提交 GET 请求以获取每个账户的详细信息:

http://bank.org/account/AZA12093
http://bank.org/account/ADK31242

就这个示例而言,鉴于客户端已经拥有发起账户转账所需的信息,那么就让我忽略这些请求。

步骤 3:接着,客户端通过提交如下 POST 请求发起账户转账:

POST /transfers
Host: bank.org
Content-Type: application/xml;charset=UTF-8

account:AZA12093
    account:ADK31242
     100
    RESTing

服务器获得账户的路由代码(译注:由美国银行家协会在美联储监管和协助下提出的金融机构识别码,很多金融机构都有一个,主要用于银行相关的交易,转账,清算等的路由确认,由 9 位 [8 位内容 +1 位验证码] 组成,主要用于美国及北美地区。),把转账提交给执行转帐的后端系统,并返回如下内容:

201 Created
Content-Type: application/xml;charset=UTF-8

account:AZA12093
    account:ADK31242
    transfer:XTA8763
     100
    RESTing

转帐并没有结束。转账将在几个工作日后异步发生(这对于银行间交易很平常),客户端可以使用交易 ID 查询交易状态。

步骤 4:一天后,客户端提交 GET 请求来查询状态。

GET /check/XTA8763
Host: bank.org

200 OK
Content-Type: application/xml;charset=UTF-8

01
    Pending

注意,尽管这个实现使用了资源、URI、表示和 HTTP 的统一接口,但它并非 RESTful 的。因为我们将在后续小节看到,这个示例并没有利用 REST 的关键约束之一,即“超媒体即应用状态引擎”。

在试图使之 RESTful 之前,让我先试着写一份该示例关联的可能用户文档。

Bank.Org API - URIs

http://bank.org/accounts?findby=someparams

向这个 URI 提交 GET 请求可查询银行账户。它将返回一个accounts文档。详见 XML 模式。

http://bank.org/account/{account-id}

向这个 URI 提交 GET 请求可获取账户细节。这里的{account-id}是账户 ID。它将返回一个account文档。详见 XML 模式。

http://bank.org/transfers

向这个 URI 提交 POST 请求可创建一个账户转账。在请求体中包含了transfer文档。如果请求成功,服务器会返回一个transfer文档。详见 XML 模式。

http://bank.org/check/{transaction-id}

向这个 URI 提交 GET 请求可查询转账状态。这里的{transaction-id}是账户转账的 ID。它会返回一个 status XML 文档。详见 XML 模式。

这种风格的文档在如今很普遍。它包含了客户端将一直需要使用的所有 URI。它描述了客户端用每个 URI 可使用的 HTTP 方法。它还包含了表示的描述,即示例中的 XML 文档。

但是这类文档有两个问题。首先,它对任何寻找机器可读正式描述的人并没有任何帮助。缺少机器可读的描述,我们就无法构建能用于测试或以其他方式执行契约的通用软件工具。缺乏这类通用软件工具,对于那些需要部署这类工具来管理和治理他们软件的人来说,这实在是一个相当大的障碍。你可能会考虑使用 WADL ,或者甚至是 WSDL 2.0 来提供一个机器可读的等价物。

其次,同时也是更重要的,用这种方式描述服务器接口,不论是像 WADL 或者 WSDL 2.0 这样机器可读的格式,还是人类可读的格式,都违反了 REST 的两个约束。这两个约束要求(a)消息是自描述的,(b)超媒体为应用状态的引擎。怎样才能做到这些,并且为什么这样做很重要呢?

回到约束

REST 的关键约束是(a)资源标示,(b)通过表示操控资源,(c)自描述的消息,(d)超媒体即应用状态引擎。

在使用 HTTP 的 RESTful 应用中,消息利用两种东西实现了自描述,其一,通过使用无状态的统一接口;其二,通过使用 HTTP 报头(Header),它描述了消息内容,除此之外还包括 HTTP 实现相关的各协议方面(如内容协商、针对缓冲的条件请求和优化并发等等)。

通过检查使用的 HTTP 方法和请求 / 响应报头,像代理或缓存这样的中间实体就能够破译哪部分协议(即 HTTP)正在被使用以及它们是如何被使用的。这类自描述信息保证了客户端和服务器之间的交互是可见的(如,对缓存的使用),可靠的(如检测局部故障并从中恢复)和可伸缩的。

第四个约束,即“超媒体即应用状态引擎”,有两个用途。第一,它不要求协议(即 HTTP)是有状态的。第二,它使服务器可以演变(如,通过引入新的 URI)并保持了客户端跟服务器间的松耦合。

服务器要是象前一节那样提供表示的描述,它就没有利用 HTTP 自描述的特性。在 HTTP 中,客户端和服务器使用“媒体类型(media type)”,或者是那些我们在请求 / 响应报头中看到的 Content-Type 头信息来描述消息内容,而不是 XML 模式。媒体类型类似于对象的类或者 XML 元素的模式类型(schema type)。

此外,如果服务器把所有 URI 都向它的客户端描述,它就无法独立演变,而且接口会变得脆弱。URI 的任何改变都有可能让现有客户端无法正常工作。但是,你怎样才能在对客户端需要连接的 URI 一无所知的情况下编写客户端呢?

答案就是使用具有已知关系的链接。链接是一种间接机制,客户端可以用它来在运行时发现 URI。一个链接至少有两个属性——URI 和关系。URI 指向资源或者资源的表示,而关系则描述了链接的类型或种类。一个真正的 RESTful 服务器应用是通过在其表示中包含预定义关系的链接来把 URI 传给客户端。于是,客户端可以无需预先了解所有 URI,而是在运行时从链接中抽取出 URI。由此,服务器可以自由地改变 URI,或者甚至在相同或者其他提供兼容性行为的服务器上引入新 URI。

最后,通过告知客户端随后要做的事,服务器在表示中返回的链接可能是上下文相关的。 换句话说,链接以一种运行时工作流的形式动态地描述了客户端和服务器之间的契约。

总而言之,对于 RESTful 应用来说,契约包含三个不同部分:统一接口、表示的媒体类型和资源的上下文相关链接。

听起来有些像童话?为了实际地展示这种契约,我会重写上面的示例。

重写示例

步骤 0:同前。

步骤 1:客户端使用相同的 URI——http://bank.org/accounts?findby=someparams 搜索账户。这次,让服务器返回不同类型的响应。

200 OK
Content-Type: application/vnd.bank.org.account+xml;charset=UTF-8

 AZA12093
        993.95
    
     ADK31242
        534.62
    

在这个响应中,请注意 Content-Type 报头的值,以及包含 URI 的链接(link)。

步骤 2:如果客户端希望了解每个账户的更多内容,它可以从上述响应的“self”关系的链接中抽取出账户 URI,向这些 URI 提交 GET 请求。

步骤 3:为了发起账户转账,客户端从上述两个账户中任选一个,并从具备“http://bank.org/rel/transfer”和“edit”关系的链接中抽取出 URI,向之提交一个 POST 请求。

POST /transfers
Host: bank.org
Content-Type: application/vnd.bank.org.transfer+xml;charset=UTF-8

account:AZA12093
    account:ADK31242
     100
    RESTing

同样请注意 Content-Type 报头的值。

发起账户转账之后,服务器返回如下内容:

201 Created
Content-Type: application/vnd.bank.org.transfer+xml;charset=UTF-8

transfer:XTA8763
     100
    RESTing

步骤 4:要想查询账户转账的状态,客户端可以从关系为“http://bank.org/check/XTA8763”的链接中抽取 URI,并向它提交一个 GET 请求。

这个实现是 RESTful 的,因为它使用了包含上下文相关链接的表示来封装交互状态,即利用了“超媒体即应用状态引擎”这条约束。

现在,让我回顾并强调实现这种新交互集合所需的信息。首先,客户端需要知道查询账户的 URI。接着,它需要知道各种链接关系的名字和语义。它还需要知道每个媒体类型的细节。它可以在运行时动态算出契约的剩余部分。因而,我们可以提供如下修订后的文档。

Bank.Org API

URIs

http://bank.org/accounts?findby=someparams

向这个 URI 提交 GET 请求可查询银行账户。你可以传递客户 ID 或客户名或客户的社保号码,将其作为 findby 的查询参数值。这个资源支持 application/vnd.bank.org.accounts+xml 媒体类型。

链接类型

self

带有这个关系的链接,其 URI 指向包含该链接的资源,如账户和转账资源。

http://bank.org/rel/transfer and edit

带有这些关系的链接,其 URI 能用于创建新的账户转账资源。

http://bank.org/rel/customer

带有这个关系的链接,其 URI 能用于获取一个客户资源。

http://bank.org/rel/transfer/from

带有这个关系的链接,其 URI 标识转账的源账户资源。

http://bank.org/rel/transfer/to

带有这个关系的链接,其 URI 标识转账的目标账户资源。

http://bank.org/rel/transfer/status

带有这个关系的链接,其 URI 能用于获取状态资源。

媒体类型

application/vnd.bank.org.accounts+xml

这个媒体类型的表示包含了在 urn:org:bank:accounts 名字空间内声明的 accounts 文档。详见 XML 模式。

application/vnd.bank.org.transfer+xml

这个媒体类型的表示包含了在 urn:org:bank:accounts 名字空间内声明的 transfer 文档。详见 XML 模式。

application/vnd.bank.org.customer+xml

这个媒体类型的表示包含了在 urn:org:bank:customer 名字空间内声明的 customer 文档。详见 XML 模式。

application/vnd.bank.org.status+xml

这个媒体类型的表示包含了在 urn:org:bank:transfer 名字空间内声明的 status 文档。详见 XML 模式。

不同类型的描述

我在上节所采用的描述 RESTful 应用的方法不仅具有某些有趣的特性,亦有些古怪。

对于那些熟悉 WSDL 和 WADL 的人来说,上节的描述可能看起来有些不合常理。我们在其中并未看到关于每个操作输入和输出消息的描述,而看到了媒体类型。但是,鉴于像application/xml这样通用的媒体类型实在是太通用了,无法帮助我们区分账户资源的表示、客户资源或者转账资源的表示。故而在这个示例中,我使用了自定义的媒体类型。每个媒体类项都以“+xml”结尾,并且按照 RFC 3023 进行描述,XML 处理器(包括 XMLHttpRequest )能够把表示当作 XML 一样进行处理。通过查看这样的媒体类型,客户端就知道收到的表示是一个账户资源,还是一个转账资源。更重要的是,它不必对它用来获取那个表示的 URI 进行任何结构或者语法方面的假设。

此外,文档并没有列出应用正在使用的所有的 URI,而仅仅包含了账户转账客户端需要发起交互的一个 URI。注意,在不同的示例中,我们或许需要记录 多个 URI。其思想是保证预发布 URI 的数量最小。为什么这样更好?原因在于,它解耦了客户端和资源的实际 URI,客户端直到运行时才需要知道其余的 URI。

最后,上述文档没有包括每个 URI 上可用的 HTTP 操作。相反,我假定客户端会向每个 URI 都提交一个 HTTP OPTIONS 以发现各种可能的操作,接着使用 HTTP GET 获取资源的表示,使用 HTTP POST 在资源集合内创建一个新资源,使用 HTTP PUT 更新现有资源(或者如果客户端可以为资源分配 URI,就创建一个),使用 HTTP DELETE 删除资源。

总而言之,要以 RESTful 方式描述契约必须:

  • 预发布一些 URI 并记录这些 URI 相应的资源。尽量保证这个列表的长度最短。这些 URI 是应用的起点。
  • 记录所有媒体类型。对于基于 XML 的表示,如果需要,使用 XML 模式记录每个表示的结构。
  • 记录所有链接的关系。
  • 让客户端在运行时使用 HTTP OPTIONS 去发现某 URI 支持的 HTTP 操作。在某些情况下,链接关系的类型足以让客户端确定服务器是否支持某个操作。

这种描述既不完整,也不是完全机器可读的。

说它不是完整的,是因为它仅仅包含了契约的静态部分,让服务器在运行时通过链接描述可能的工作流。

对于那些已经对 REST 好处深信不疑并且使用 HTTP 积极构建 RESTful 应用的人来说,缺乏完整的机器可读描述可能无关紧要。

但是对于那些正在使用类 RPC 方法(使用 SOAP、WSDL 和 WS-*)构建分布式应用以及正在考虑 REST 的人来说,缺乏完整机器可读的描述可能就是个障碍了。然而,使用 RESTful 的机器可读描述可做的工作量,即使存在的话,其作用也有限。这归结于如下原因:

  • 契约的动态天性:正如我在上节示例中所描述的,表示通过在表示中使用链接描述了契约的动态方面。用表示之外、某些基于 XML 的机器可读描述来解释上下文相关的契约是多此一举。
  • 媒体类型的灵活性:与基于 SOAP 的应用不同,RESTful 应用不限于使用 XML。它们可以使用其他没有模式语言的媒体类型。

同样需要注意,为远程接口描述一个完全机器可读的描述契约是一种谬论。用 WSDL 或 WADL 创建的机器可读描述仅能描述结构和语法,而不能描述语义。但机器可读的描述有时能降低我们作为程序员、测试员和管理员需要做的工作量。

要是我们把统一接口和契约的动态方面搁置一边,我们可以用机器可读方式描述契约的剩余部分。以下就有一个示例。注意,在这个描述中,我的意图只是想帮助那些要监测或测试客户端 / 服务器端交互的工具和框架,当然不是要模仿 WSDL 或 WADL。

application/vnd.bank.org.accounts+xml
            bank:account
        
        application/vnd.bank.org.transfer+xml
            bank:transfer
        
        ...
    

    This relation ...
            http://bank.org/rel/transfer
        
        ...
    

    accounts
            application/vnd.bank.org.accounts+xml
            http://bank.org/accounts
                Use this parameter to ...
                        findBy
                    
                
            
        
        transfer
            application/vnd.bank.org.transfer+xml
        
        ...
    

这是我在前节所描述的契约的机器可读版本,很明显,它并不符合任何标准。这个描述并没有消除对于人类可读描述的需要,因为我们仍然需要描述应用语义。

让我强调一下这个描述中的关键部分:

  • 模式类型: 由于在这个示例中,我为所有的表示选择使用 XML,因而包含了所使用模式的引用。如果模式无法描述所选的表示格式,这部分将毫无意义。
  • 媒体类型以及所使用的相应 XML 文档。
  • 所有链接关系的列表。
  • 资源和它们媒体类型的名字。注意,这些名字不是 URI。
  • 为应用提供起始点的资源的 URI。

这种描述比人类可读的描述更有用吗?由于缺乏可解释这种描述的工具和框架,答案可能是否定的。

这种方法实用吗?

如果你正在编写基于机器可读契约(如 WADL 文档)的服务器端代码和客户端代码,编码流程可能如下:

  • 从描述生成资源类。每个资源类都潜在对应于描述中的 URI。
  • 生成跟表示绑定的类。如果表示是基于 XML 的,我会生成跟多种 XML 文档匹配的类。
  • 生成跟多种 HTTP 操作匹配的客户端存根(stub)。
  • 开始编码

这个模型对以 RESTful 方式描述契约并不适用,步骤会有所不同:

  1. 读取所有媒体类型描述。如果媒体类型是用 XML 模式描述的,那么就获取这个模式,生成能够解析或者生成 XML 的类。
  2. 读取所有链接关系的描述。
  3. 手工创建资源类。
  4. 每当客户端接收到一个表示时,除了从表示中抽取数据之外,还要查看链接。如果你找到一个包含已知关系的链接,那就抽取hreftype(如果有的话)属性以备后用。
  5. 在客户端,在发送 HTTP 请求之前,先发送一个 HTTP OPTION 请求检查服务器端是否支持你要执行的操作。如果支持,在你的客户端应用中激活该操作。

我关注的大多数软件框架都可以处理部分上述步骤(如通用接口或资源类的约定),而且还能生成创建或解析 XML 的类(这取决于你所选的编程语言)。但 是剩余部分就留给了开发者。更有甚者,这类框架多数强调服务器端编程,并在假设现有 HTTP 客户端库已经足够使用的情况下忽略了对客户端编程的考虑。因 而,在处理上述 (4) 和 (5) 项时,可能需要创建自定义代码。

对于那些想要测试或者增强契约的软件工具怎么办?创建这种工具,让其在运行时读取上述机器可读描述以完成如下工作,是可行的。

  • 检查表示的媒体类型是否是预定义的
  • 检查表示是否匹配媒体类型的预定义描述
  • 检查表示中包含的所有链接是否有预定义关系和媒体类型,并检查所包含 URI 是否符合预定义的 URI 模式。

我还没有听说哪个软件能以这种方式来完成以上验证。但是,出现的机会很大。如果你读到文章的这里,你就会明白那些机会是什么。

结论

我写这篇文章的一个目的是要阐述这样的事实:像 WSDL 和 WADL 这样的传统契约描述并不适合描述 RESTful 应用。正如我在账户转账示例中所示 范的,只有部分契约能被静态地描述,其余都是动态并上下文相关的。客户端可以通过在运行时查看链接来遵循契约的动态部分。你可以出于设计时和测试的目的试 着用某些机器可读文档来描述前一部分,但是让服务器在运行时描述其余部分会大大降低客户端和服务器之间的耦合。试图静态地描述完整契约无异于会使所有上下 文相关的链接在表示之外重复一遍。

相反的,诸如 WSDL 和 WADL 这样的描述语言试图用上下文无关的方式描述契约,并把用户文档留给客户端开发者,以便他们能够学习如何从那些描述中描述的各类消息交互模式合成客户端应用。在 RESTful 应用中,服务器在运行时以链接形式提供这个信息。

总之,RESTful 是有契约的。我们只需要知道如何找到并在哪儿找到该契约,同时谨记该契约是上下文相关的,就行了。

关于作者

Subbu 在 Yahoo 工作。通过他的博客可以了解关于他的更多信息。

查看英文原文: Describing RESTful Applications


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论