BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

描述RESTful应用程序

| 作者 Subbu Allamaraju 关注 1 他的粉丝 ,译者 胡键 关注 0 他的粉丝 发布于 2009年2月5日. 估计阅读时间: 31 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

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账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

好! by Liu Dun

好文章。收藏了。谢谢了。

讲的不错 by Jia Bruce

提供最小的一组URI作为初始,其他的URI通过动态发现。这个就是主要思想了。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT