Roy Fielding最近这样说道:
要抓住这类陈述的要领不是件简单的事。如果服务器不将它自己的名字空间控制在一个固定的资源层次下,客户端及更重要的客户端开发者将如何知道或发现资源的URI呢?毕竟,长久以来,分布式客户端/服务器开发的一个基本假设就是:为了构建、维护和管理这类应用, 我们需要预先对应用的接口正式描述。Roy Fielding的观点似乎跟这个假设相冲突。 关于描述RESTful系统的讨论并非新鲜事物。这类讨论几乎总会得出类似上述的观点。例如,看看前年infoQ上关于争论:REST需要描述语言么?的备忘录,它总结了当时发生的部分讨论。今天的事态并没有什么特别的不同。 针对RESTful应用的正式描述语言,虽然有大量的赞成和反对意见,但像WADL这样的描述语言只得到了有限的发展。然而,由于缺乏一种机器能够解释的“标准”语言,服务器应用所采取的最常用方法就是记录所有URI、支持的HTTP方法和表示(representation)的结构(如,对应的XML和JSON格式),这样客户端应用开发者就能依赖这种文档来编写代码。 但是,这种方式跟REST的一些基本原则(如Roy Fielding在上面所说的)有冲突。即便我们无视这一异议,对于那些试图通过HTTP RESTful构建分布式应用的人来说,基本问题仍然存在。不正式地定义契约,服务器怎么可能得以脱身?没有契约,我们如何能确定正确实现了客户端和服务器——不仅正确实现了各自的设计规范,而且恰当地实现了其他业务/技术策略? 用HTTP作为应用协议、以RESTful方式构建的分布式应用其实有一个契约,但其性质和种类却不相同。我们需要知道寻找的目标和位置。如果我们打算提出一种描述语言,那么它就要和Roy Fielding所说的保持一致,它不能是类似WSDL或WADL这样的东西。在这篇文章中,我的目标是回答如下问题:
请让我从一个示例开始。 示例任务是写一个客户端程序,实现同一位客户在不同银行账户间的转账业务。 首先让我描述一下客户端和服务器之间的所有交互,接着看看这个契约的可能描述。 步骤 0:用户登录客户端。为了保持此次讨论的重点,请让我忽视所有安全方面的内容。 步骤 1:客户端使用URI: 200 OK Content-Type: application/xml;charset=UTF-8 <accounts xmlns="urn:org:bank:accounts"> <account> <id>AZA12093</id> <customer-id>7t676323a</customer-id> <balance currency="USD">993.95</balance> </account> <account> <id>ADK31242</id> <customer-id>7t676323a</customer-id> <balance currency="USD">534.62</balance> </account> </accounts> 我们假设跟名字空间 步骤 2:由于客户端知道两个账户的ID,在必要情况下,它可以向以下URI提交GET请求以获取每个账户的详细信息: http:///account/AZA12093 http:///account/ADK31242 就这个示例而言,鉴于客户端已经拥有发起账户转账所需的信息,那么就让我忽略这些请求。 步骤 3:接着,客户端通过提交如下POST请求发起账户转账: POST /transfers Host: Content-Type: application/xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <from>account:AZA12093</from> <to>account:ADK31242</to> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 服务器获得账户的路由代码(译注:由美国银行家协会在美联储监管和协助下提出的金融机构识别码,很多金融机构都有一个,主要用于银行相关的交易,转账,清算等的路由确认,由9位[8位内容+1位验证码]组成,主要用于美国及北美地区。),把转账提交给执行转帐的后端系统,并返回如下内容: 201 Created Content-Type: application/xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <from>account:AZA12093</from> <to>account:ADK31242</to> <id>transfer:XTA8763</id> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 转帐并没有结束。转账将在几个工作日后异步发生(这对于银行间交易很平常),客户端可以使用交易ID查询交易状态。 步骤 4:一天后,客户端提交GET请求来查询状态。 GET /check/XTA8763 Host: 200 OK Content-Type: application/xml;charset=UTF-8 <status xmlns="urn:org:bank:accounts"> <code>01</code> <message xml:lang="en">Pending</message> </status> 注意,尽管这个实现使用了资源、URI、表示和HTTP的统一接口,但它并非RESTful的。因为我们将在后续小节看到,这个示例并没有利用REST的关键约束之一,即“超媒体即应用状态引擎”。 在试图使之RESTful之前,让我先试着写一份该示例关联的可能用户文档。
这种风格的文档在如今很普遍。它包含了客户端将一直需要使用的所有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:///accounts?findby=someparams搜索账户。这次,让服务器返回不同类型的响应。 200 OK Content-Type: application/vnd..account+xml;charset=UTF-8 <accounts xmlns="urn:org:bank:accounts"> <account> <id>AZA12093</id> <link href="http:///account/AZA12093" rel="self"/> <link rel="http:///rel/transfer edit" type="application/vnd..transfer+xml" href="http:///transfers"/> <link rel="http:///rel/customer" type="application/vnd..customer+xml" href="http:///customer/7t676323a"/> <balance currency="USD">993.95</balance> </account> <account> <id>ADK31242</id> <link href="http:///account/ADK31242" rel="self"/> <link rel="http:///rel/transfer" type="application/vnd..customer+xml" href="http:///transfers"/> <link rel="http:///rel/customer" type="application/vnd..customer+xml" href="http:///customer/7t676323a"/> <balance currency="USD">534.62</balance> </account> </accounts> 在这个响应中,请注意Content-Type报头的值,以及包含URI的链接(link)。 步骤 2:如果客户端希望了解每个账户的更多内容,它可以从上述响应的“self”关系的链接中抽取出账户URI,向这些URI提交GET请求。 步骤 3:为了发起账户转账,客户端从上述两个账户中任选一个,并从具备“http:///rel/transfer”和“edit”关系的链接中抽取出URI,向之提交一个POST请求。 POST /transfers Host: Content-Type: application/vnd..transfer+xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <from>account:AZA12093</from> <to>account:ADK31242</to> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 同样请注意Content-Type报头的值。 发起账户转账之后,服务器返回如下内容: 201 Created Content-Type: application/vnd..transfer+xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <link rel="self" href="http:///transfer/XTA8763"/> <link rel="http:///rel/transfer/from" type="application/vnd..account+xml" href="http:///account/AZA12093"/> <link rel="http:///rel/transfer/to" type="application/vnd..account+xml" href="http:///account/ADK31242"/> <link rel="http:///rel/transfer/status" type="application/vnd..status+xml" href="http:///check/XTA8763"/> <id>transfer:XTA8763</id> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 步骤 4:要想查询账户转账的状态,客户端可以从关系为“http:///check/XTA8763”的链接中抽取URI,并向它提交一个GET请求。 这个实现是RESTful的,因为它使用了包含上下文相关链接的表示来封装交互状态,即利用了“超媒体即应用状态引擎”这条约束。 现在,让我回顾并强调实现这种新交互集合所需的信息。首先,客户端需要知道查询账户的URI。接着,它需要知道各种链接关系的名字和语义。它还需要知道每个媒体类型的细节。它可以在运行时动态算出契约的剩余部分。因而,我们可以提供如下修订后的文档。
不同类型的描述我在上节所采用的描述RESTful应用的方法不仅具有某些有趣的特性,亦有些古怪。 对于那些熟悉WSDL和WADL的人来说,上节的描述可能看起来有些不合常理。我们在其中并未看到关于每个操作输入和输出消息的描述,而看到了媒体类型。但是,鉴于像 此外,文档并没有列出应用正在使用的所有的URI,而仅仅包含了账户转账客户端需要发起交互的一个URI。注意,在不同的示例中,我们或许需要记录 多个URI。其思想是保证预发布URI的数量最小。为什么这样更好?原因在于,它解耦了客户端和资源的实际URI,客户端直到运行时才需要知道其余的 URI。 最后,上述文档没有包括每个URI上可用的HTTP操作。相反,我假定客户端会向每个URI都提交一个HTTP OPTIONS以发现各种可能的操作,接着使用HTTP GET获取资源的表示,使用HTTP POST在资源集合内创建一个新资源,使用HTTP PUT更新现有资源(或者如果客户端可以为资源分配URI,就创建一个),使用HTTP DELETE删除资源。 总而言之,要以RESTful方式描述契约必须:
这种描述既不完整,也不是完全机器可读的。 说它不是完整的,是因为它仅仅包含了契约的静态部分,让服务器在运行时通过链接描述可能的工作流。 对于那些已经对REST好处深信不疑并且使用HTTP积极构建RESTful应用的人来说,缺乏完整的机器可读描述可能无关紧要。 但是对于那些正在使用类RPC方法(使用SOAP、WSDL和WS-*)构建分布式应用以及正在考虑REST的人来说,缺乏完整机器可读的描述可能就是个障碍了。然而,使用RESTful的机器可读描述可做的工作量,即使存在的话,其作用也有限。这归结于如下原因:
同样需要注意,为远程接口描述一个完全机器可读的描述契约是一种谬论。用WSDL或WADL创建的机器可读描述仅能描述结构和语法,而不能描述语义。但机器可读的描述有时能降低我们作为程序员、测试员和管理员需要做的工作量。 要是我们把统一接口和契约的动态方面搁置一边,我们可以用机器可读方式描述契约的剩余部分。以下就有一个示例。注意,在这个描述中,我的意图只是想帮助那些要监测或测试客户端/服务器端交互的工具和框架,当然不是要模仿WSDL或WADL。 <description xmlns:bank="urn:org:bank:accounts"> <types> <!-- Include the schema used for all representations --> <include href="bank-schema.rng"/> </types> <!-- List all media types and the corresponding XML types --> <media-types> <media-type> <name>application/vnd..accounts+xml</name> <representation>bank:account</representation> </media-type> <media-type> <name>application/vnd..transfer+xml</name> <representation>bank:transfer</representation> </media-type> ... </media-types> <relations> <relation> <documentation>This relation ...</documentation> <name>http:///rel/transfer</name> </relation> ... </relations> <resources> <resource> <name>accounts</name> <media-type-ref>application/vnd..accounts+xml</media-type-ref> <uri> <!-- This is optional --> <base>http:///accounts</base> <params> <param> <documentation>Use this parameter to ...</documentation> <name>findBy</name> </param> </params> </uri> </resource> <resource> <name>transfer</name> <media-type-ref>application/vnd..transfer+xml</media-type-ref> </resource> ... </resources> </description> 这是我在前节所描述的契约的机器可读版本,很明显,它并不符合任何标准。这个描述并没有消除对于人类可读描述的需要,因为我们仍然需要描述应用语义。 让我强调一下这个描述中的关键部分:
这种描述比人类可读的描述更有用吗?由于缺乏可解释这种描述的工具和框架,答案可能是否定的。 这种方法实用吗?如果你正在编写基于机器可读契约(如WADL文档)的服务器端代码和客户端代码,编码流程可能如下:
这个模型对以RESTful方式描述契约并不适用,步骤会有所不同:
我关注的大多数软件框架都可以处理部分上述步骤(如通用接口或资源类的约定),而且还能生成创建或解析XML的类(这取决于你所选的编程语言)。但 是剩余部分就留给了开发者。更有甚者,这类框架多数强调服务器端编程,并在假设现有HTTP客户端库已经足够使用的情况下忽略了对客户端编程的考虑。因 而,在处理上述(4)和(5)项时,可能需要创建自定义代码。 对于那些想要测试或者增强契约的软件工具怎么办?创建这种工具,让其在运行时读取上述机器可读描述以完成如下工作,是可行的。
我还没有听说哪个软件能以这种方式来完成以上验证。但是,出现的机会很大。如果你读到文章的这里,你就会明白那些机会是什么。 结论我写这篇文章的一个目的是要阐述这样的事实:像WSDL和WADL这样的传统契约描述并不适合描述RESTful应用。正如我在账户转账示例中所示 范的,只有部分契约能被静态地描述,其余都是动态并上下文相关的。客户端可以通过在运行时查看链接来遵循契约的动态部分。你可以出于设计时和测试的目的试 着用某些机器可读文档来描述前一部分,但是让服务器在运行时描述其余部分会大大降低客户端和服务器之间的耦合。试图静态地描述完整契约无异于会使所有上下 文相关的链接在表示之外重复一遍。 相反的,诸如WSDL和WADL这样的描述语言试图用上下文无关的方式描述契约,并把用户文档留给客户端开发者,以便他们能够学习如何从那些描述中描述的各类消息交互模式合成客户端应用。在RESTful应用中,服务器在运行时以链接形式提供这个信息。 总之,RESTful是有契约的。我们只需要知道如何找到并在哪儿找到该契约,同时谨记该契约是上下文相关的,就行了。 关于作者Subbu在Yahoo工作。通过他的博客可以了解关于他的更多信息。 查看英文原文:Describing RESTful Applications。 |
|