65.9K
CodeProject 正在变化。 阅读更多。
Home

REST Web 服务的一切——是什么和如何做——第 2 部分——设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (74投票s)

2007年11月10日

CPOL

16分钟阅读

viewsIcon

286962

本文是我关于 REST Web 服务系列文章的第二部分。第一篇文章介绍了 REST Web 服务,而本文将带我们完成设计一个 RESTful Web 服务的过程。

设计 RESTful Web 服务步骤


将拼图的碎片拼凑起来

在我上一篇文章中,我介绍了 REST Web 服务的概念。在急于着手实现或编写 REST Web 服务之前,我们首先需要停在设计阶段。我知道许多开发人员更热衷于在花费一些时间进行proper design之前就开始编码。他们有时会忽略一个事实:你不需要 Visual Studio 来进行设计;白板就足够了。

给定一个 Web 服务需求,REST 设计的第一步是确定我们将要暴露的对象及其各自的表示(representation)。对象的表示可以被认为是其属性的集合。表示需要仔细设计,我们很快就会看到这一点。

让我们尝试基于经典的员工管理示例来构建。想象一个旨在提供员工管理界面的 Web 服务。这是一个好的开始,因为要暴露的对象很容易达成一致——员工。请记住,在 REST 中,我们暴露的是对象而不是方法。

下一步是确定对象的表示——对于我们的示例,一个简单的 XML 结构是否足够?

<Employee id='25648'>
    <Name>Peter Scheufele</Name>
    <Address>123 Main St. Dallas TX 75226</Address>
    <Designation>System Analyst</Designation>
    <HireDate>08/20/2007</HireDate>
</Employee>

在这里,我需要强调几个关于表示真正含义以及如何设计它的重要观点。为了让我能够做到这一点,让我们来研究一下资源(resource)URI的概念。

在客户服务时代,REST Web 服务最大的推动力在于它应该让客户端更容易使用。如果客户端可以直接在 URL 中输入他们想要操作的对象,那么对客户端来说会变得多么容易?

想象一下,一位人力资源专员想要查看员工 Peter Scheufele 的详细信息——她在浏览器地址栏中输入:

http://employeeinfo/v1.4/employee/12098

她必须知道 12098 是 Peter 的员工 ID。即使她不知道,那么 URL

http://employeeinfo/v1.4/employees?name=peter

并获得一个名字中包含“peter”的员工列表,怎么样?

注意上面两个 URL 中的 **employee** 和 **employees** 这两个词。它们是资源(resources)资源是 REST Web 服务暴露并使用 URL 本身访问的对象。在这个例子中,Web 服务暴露了两个对象——employee,代表一个员工,以及employees,代表员工的集合。严格来说,这些地址是URI,而不是URL。因为它们比定位资源做得更多,它们实际上是**标识资源**。这就是为什么它们是**统一资源标识符(uniform resource identifiers)**。

资源的表示应该被视为资源的状态。如果表示发生变化,那就是对该资源进行了 UPDATE 操作。表示通常是在响应 GET 请求时返回的。我们将employee对象的表示设为 XML。必须是 XML 吗?不。它可以是任何东西,但 XML 很难被超越。好的 REST Web 服务除了 XML,也支持 JSON,但我们先坚持使用 XML,以便理清基本概念。

回到 HTTP 动词,REST 建立了一个基本指南:Web 服务应该如何响应 HTTP 动词 GET、PUT、POST 和 DELETE?以下是这些指南:



  • GET:**返回**(检索)URI 上标识的资源的表示

示例

客户端 --> 服务器:客户端调用我们的 Web 服务以获取 Peter Scheufele 的详细信息:

GET http://employeeinfo/v1.4/employee/12098 HTTP/1.1
Accept: */*
Accept-Language: en-us
Authorization: hisyi76985==03784j3bkngdfyuwetfiuw
User-Agent: Client Application developed by the super coders
Host: supercoders
Connection: Keep-Alive


服务器 --> 客户端:服务器响应 Peter Scheufele 的表示

HTTP/1.1 200 Ok
Server: Apache-Coyote/1.1
Last-Modified: Thu, 11 Oct 2007 14:45:35 GMT
ETag: 12098
Location: "http://employeeinfo/v1.4/employee/12098">http://employeeinfo/v1.4/employee/12098 
Date: Fri, 12 Oct 2007 12:00:28 GMT
Connection: Close
Content-Type: text/xml; charset=UTF-8
Content-Length: 2324

<Employee id='12098'>
    <Name>Peter Scheufele</Name>
    <Address>123 Main St. Dallas TX 75226</Address>
    <Designation>System Analyst</Designation>
    <HireDate>08/20/2007</HireDate>
</Employee>

注意:请注意,请求没有 HTTP 主体,但响应包含 Peter Scheufele 的表示作为 HTTP 主体,并且它将一些特定的 HTTP 标头设置为特定值——这些是REST 的最佳实践



  • PUT:**更新** URI 上标识的资源的表示

示例

客户端 --> 服务器:客户端调用我们的 Web 服务以更新 Peter Scheufele 的地址:

PUT http://employeeinfo/v1.4/employee/12098 HTTP/1.1
Accept: */* 
Accept-Language: en-us
Authorization: hisyi76985==03784j3bkngdfyuwetfiuw
User-Agent: Client Application developed by the super coders
Host: supercoders
Connection: Keep-Alive
Content-Type: text/xml; charset=UTF-8
Content-Length: 354

<Employee id='12098'>
    <Address>6777 Spring Creek Dr. Fairview TX - 56458</Address>
</Employee>

服务器 -->客户端:服务器更新 Peter Scheufele 的地址并响应 Peter Scheufele 的表示

HTTP/1.1 200 Ok
Server: Apache-Coyote/1.1
Last-Modified: Thu, 11 Oct 2007 14:45:35 GMT
ETag: 12098
Location: "http://employeeinfo/v1.4/employee/12098">http://employeeinfo/v1.4/employee/12098 
Date: Fri, 12 Oct 2007 12:00:28 GMT
Connection: Close
Content-Type: text/xml; charset=UTF-8
Content-Length: 2214

<Employee id='12098'>
    <Name>Peter Scheufele</Name>
    <Address>6777 Spring Creek Dr. Fairview TX - 56458</Address>
    <Designation>System Analyst</Designation>
    <HireDate>08/20/2007</HireDate>
</Employee>

注意:请注意,请求将员工表示 XML 作为 HTTP 主体发送,但有趣的是,它只发送需要更新的信息。这不是任何指南的一部分——这取决于您,Web 服务的设计者,来决定——您是期望客户端在更新时发送整个表示 XML,还是只发送需要更新的元素。一个架构良好的系统通常会使用 XSD 来验证传入的 XML,因此 XML 模式需要设计成允许增量更新 XML。这里我们已经显著偏离了纯粹的 REST 讨论,因为这些讨论是应用程序特定的设计,但我认为这些对于强调 REST 的实际方面是必要的。

请注意,响应会携带 Peter Scheufele 的完整表示,这是 REST 的最佳实践。当资源的表示被更新时,Web 服务应该在成功更新后将完整的表示发回给客户端。同样,这不是一个“不能违反”的规则,只是推荐。


另外两个动词:POST 和 DELETE

既然我们已经看到了 GET 和 PUT 的实际应用,现在是时候处理 POST 了。在深入研究 POST 之前,让我们快速回顾一下上面已经提到的 GET 和 PUT 的定义:

GET:返回(检索)URI 上标识的资源的表示

PUT:更新 URI 上标识的资源的表示

注意这两个动词是如何作用于资源的表示的?您是否还记得上一篇文章中解释的 REST 的高层目标?就是用少数标准动词作用于暴露的名词的整个想法?我们在这里看到的正是这样:GET 和 PUT 以一种标准、预定义的方式作用于资源employee/12098:GET 检索它,PUT 更新它。

为了更准确地说,GET 检索了表示,PUT 更新了表示。因此,我们现在对表示有了更清晰的认识:它充当资源的别名——**HTTP 动词实际作用的部分**。

话虽如此,让我们转向 POST。在 REST 中,**POST 意味着创建新资源**。您能想象一个请求作用于某个现有资源的表示吗?怎么可能?

因此,如果客户端对 **http://employeeinfo/v1.4/employee/12098** 执行 POST 操作,是否有意义?显然不是——因为要创建的新资源与 12098(Peter Scheufele)无关。

如果 POST 请求发送到 **http://employeeinfo/v1.4/employee** 呢?嗯——虽然这有点意义,但我们仍然难以强制执行规则——**REST 请求应该作用于 URI 所标识的资源的表示**。首先,这里的 URI 并没有标识任何特定的员工,也没有单独的employee的表示。

这正是集合资源(collection resource)的必要性所在。我们在想象一位人力资源员工摆弄 REST 时,已经简要提到了集合资源。

集合资源有自己的表示,其特点是它不需要任何特定的实例即可访问。让我们为我们的 Web 服务暴露第二个对象:

employees

作为 employee 的复数形式,employees 是一个集合资源,具有以下表示:

<Employees>
    <Employee id='12098'>
        <Name>Peter Scheufele</Name>
        <Address>6777 Spring Creek Dr. Fairview TX - 56458</Address>
        <Designation>System Analyst</Designation>
        <HireDate>08/20/2007</HireDate>
    </Employee>
    <Employee id='58585'>
        <Name>Tina Sreenivas</Name>
        <Address>324 Valley View Rd. Anna TX - 75848</Address>
        <Designation>HR Analyst</Designation>
        <HireDate>01/10/2005</HireDate>
    </Employee>
    <!-- Multiple Employee element supported -->
</Employees> 

不一定非要使用复数作为集合资源的名称——我们可以称之为EmployeesCollAllEmployees或其他任何名称。此外,如果每个employee的单独表示过于庞大,将每个employee的完整表示打包到employees中可能会不切实际且性能下降。在这种情况下,我们可以定义employees的表示如下:

<Employees>
    <Employee id='12098'>
    "http://employeeinfo/v1.4/employee/12098">http://employeeinfo/v1.4/employee/12098
    </Employee>
    <Employee id='58585'>
    http://employeeinfo/v1.4/employee/58585
    </Employee>
    <!-- Multiple Employee element supported -->
</Employees>

此表示仅打包集合中每个员工的 URI,而不是每个员工的完整表示。然后,客户端需要为每个员工发出 GET 命令(在其各自的 URI 上)以获取单独的数据。

请注意,暴露的employees对象不需要指定任何特定的员工 ID——因此,它可以被视为一个全局资源。如果客户端对 employees 执行 GET 操作,它将如下所示:

GET http://employeeinfo/v1.4/employees HTTP/1.1

响应将包含employees的完整表示,其中包含所有员工。

在 REST 中,您还可以定义自己的查询字符串参数。您可以使用 GET 和employees资源组合来暴露一个**搜索**功能。例如,您可以支持查询字符串参数**name**和**address**。作为响应,您返回的表示 XML 可能只包含满足搜索条件的那些员工。例如:

GET http://employeeinfo/v1.4/employees?name='george' HTTP/1.1

上面的请求将返回名字中包含“george”的所有员工的列表。

突然,一系列新的无限可能性出现了——不是吗?通过您定义的资源、您设计的表示以及您设计的查询字符串参数的巧妙组合——世界尽在您的掌握!虽然传统的 Web 服务的方法提供了一种单调乏味的单边暴露您功能的维度——REST 为您提供了一系列有趣的组件,可以组合成令人眼花缭乱的服务——所有这些都在标准化的 HTTP 动词及其标准含义的框架内。

对此感到兴奋吗?让我们回到 POST。您可能已经猜到了——在我们的示例中,POST 动词应该只允许在employees资源上使用。

因此,请求:

POST http://employeeinfo/v1.4/employees HTTP/1.1

<Employee id=''>
    <Name>Bruce Evans</Name>
    <Address>1 ABC Rd. OK - 85748</Address>
    <Designation>Assistant Manager</Designation>
    <HireDate>11/12/2007</HireDate>
</Employee>

正在请求创建一个新员工——Bruce Evans。请注意,这如何遵循 REST 指南——**REST 请求应该作用于 URI 所标识的资源的表示**。我们是否遵循了这项指南?一字不差!看它是如何做的:这个 URI 中标识的资源是什么?Employees。它的表示是什么?包含所有employee元素的 XML。此请求**作用于该表示**,因为它在末尾创建了另一个employee节点。

关于此 POST 请求需要注意的几点:它不在请求 XML 中发送员工 ID,因为新员工尚未创建。它必须提供足够的信息,以便服务能够创建新员工。响应可以返回包含新创建的员工的employees的表示,或者可以返回新创建的employee的表示。响应应包含新员工的 ID。响应应携带 HTTP 响应代码 201 Created(而不是 200 Ok)。

DELETE 动词用于删除资源。现在我们有两个资源:employeeemployees,DELETE 可以作用于其中任何一个,但要作用于employees,它必须指定要删除哪个员工,这可以通过查询字符串**id**来完成。因此:

DELETE http://employeeinfo/v1.4/employees?id=45125 HTTP/1.1
并且
DELETE http://employeeinfo/v1.4/employee/45125 HTTP/1.1

两者都应同等对待。作为设计者,您可以将删除操作限制为其中一个——您可以选择说 DELETE 只应发送到employee资源。这样您就不必添加另一个查询字符串参数**id**了。


设计输出

到目前为止,我们的设计输出是我称之为操作矩阵。它以网格的形式列出了我们的 Web 服务如何响应不同的请求——这些请求是 HTTP 动词、URI、查询字符串参数等的组合。

动词 URI 查询字符串 请求主体 响应代码 响应主体 其他说明
GET …/employee/{id} 忽略 忽略 200 Ok ERX 如果 id 不存在,则响应 404 Not Found
PUT …/employee/{id} 忽略 ERX 200 Ok ERX 输入部分 ERX OK
POST …/employee/{id} 忽略 忽略 405 Not Allowed 错误 XML POST 禁止在此 URI 上使用
删除 …/employee/{id} 忽略 忽略 200 Ok empty 如果 id 不存在,则响应 404 Not Found
GET …/employee 忽略 忽略 400 Bad request empty GET 必须在 URI 中指定员工 ID
PUT …/employee 忽略 忽略 400 Bad request empty PUT 必须在 URI 中指定员工 ID
POST …/employee 忽略 ERX 201 Created ERX 如果请求 ERX 中的数据足够,将创建新员工。否则响应 400。
删除 …/employee 忽略 忽略 400 Bad request empty DELETE 必须在 URI 中指定员工 ID
GET …/employees name, address, id 忽略 200 Ok ESRX 如果没有查询字符串参数,则返回所有员工。如果查询字符串存在,则返回搜索结果。
PUT …/employees 忽略 忽略 405 Not Allowed errorXml PUT 不允许用于 employees URI
POST …/employees 忽略 ERX 201 Created ESRX 如果请求 ERX 中的数据足够,将创建新员工。否则响应 400。
删除 …/employees id 忽略 200 Ok empty 如果 id 无效,则响应 404;如果 id 参数缺失,则响应 400

ERX = Employee Representation XML,ESRX = Employees Representation XML。

我没有包含解释操作的列——这本可以说明第一个 PUT 记录的“更新员工数据”——绘制 HTML 表格的空间限制非常恼人。因此,这个表定义了我们 Web 服务的行为。您可以说,这就是 REST 等同于 WSDL——*契约*!这就是设计者移交给服务和客户端开发人员的东西。试图为您的特定 Web 服务需求找出这个矩阵就是本文关于 REST 设计的内容。


REST 强制性不强

现在我们已经设计了一个简单的 REST Web 服务,是时候告诉您我们使用了一些 REST 不一定强制的概念。例如,我们创建了一个集合资源,因为对employee资源执行 POST 操作没有意义。但是作为 Web 服务的设计者,您可以说,“我不在乎”。您可以将 Web 服务编写成这样:新员工的创建必须通过 POST 到employee来完成。因此,考虑如下请求:

POST http://employeeinfo/v1.4/employee HTTP/1.1

<Employee id=''>
    <Name>Bruce Evans</Name>
    <Address>1 ABC Rd. OK - 85748</Address>
    <Designation>Assistant Manager</Designation>
    <HireDate>11/12/2007</HireDate>
</Employee>

这可能是您的 Web 服务的一个有效合法请求,而且没有明显的问题。它确实略微违反了 REST 规则,即**REST 请求应该作用于 URI 所标识的资源的表示**。但这就像稍微弯曲规则。

我见过很多 Web 服务弯曲这样的规则——而且没有人因此而不高兴。只要它对客户端有意义,只要它不违反 HTTP 动词的基本原则:GET 是检索,PUT 是更新,POST 是创建,DELETE 是删除,任何 REST 设计都是可以接受的。然而,我认为完美的设计将尝试遵循最佳实践指南,而不弯曲任何规则。

因此,底线是您的资源(*对象*)定义、您的URI表示的构建,以及您的最终操作矩阵:哪个动词作用于哪个 URI 以及什么查询字符串参数会产生什么操作——这就是 REST 设计的全部内容。


复杂的操作

到目前为止,我们设计的 REST Web 服务可以有效地替换具有以下 Web 方法的传统 Web 服务:

GetEmployeeInfo, SetEmployeeInfo, AddEmployee 和 DeleteEmployee。

现在,假设传统 Web 服务还有另一个方法——

'TransferEmployee'

我们如何在 REST 中实现它?每当我们遇到这种情况时,都应该问自己:这个操作会改变资源的表示吗?是的,它会吗?在这种情况下,答案是:是的,只有当表示包含**location**时。这将导致我们修改表示 XML——我们将location添加到其中。然后我们将使用 PUT 动词来实现TransferEmployee。因为现在TransferEmployee变成了一个简单的**更新**调用——它不过是使用新位置更新表示。

有时很难回答这个问题——*这个操作会改变表示吗*?因为有些操作似乎并不真正改变任何属性。让我们想象一个用于贷款决策的 Web 服务。如果您走进一家银行要求汽车贷款,银行家通常会将审批请求提交给所有银行分行使用的中央决策制定者。让我们设想这是一个 Web 服务。让我们设想一个具有方法GetLoanApprovalDecision的传统 Web 服务——这是在银行家机器上运行的客户端调用的 Web 方法。我们将如何建模一个 REST Web 服务来完成这项工作?

在这里,设计挑战在于给我们的魔方子块恰到好处的转动——我们拥有资源、URI、表示和查询字符串参数作为原材料。

我们的 Web 服务可能有一个**customer**作为资源——但这有助于我们建模GetLoanApprovalDecision吗?帮助不大。将资源**loan**怎么样?这有助于我们创建一个表示,当我们在问——*GetLoanApprovalDecision*是否改变这个表示时,我们可以引用它吗?或者,**application**会不会是更好的暴露资源?

没有对错之分——只有更好和更差。我会选择**application**资源——表示 XML 包含申请人的个人数据和一个名为Status的部分。请求将留空,响应将填写“Approved”、“Declined”或“Referred to underwriter”。我会使用 POST 来*创建新应用程序*。创建请求将返回一个应用程序 ID。创建后,我将支持对特定应用程序资源的 GET 来获取状态。这将是我相当于GetLoanApprovalDecision——请注意,我已经将其分解为 2 个 REST 调用——POST 和 GET。POST 创建一个应用程序资源,GET 检索状态。我还可以有一个查询字符串参数**view**,将由 GET 支持,可能的值为**all**、**applicant**和**status**。虽然对“all”的响应将返回整个表示 XML,“applicant”将只返回包含申请人数据的子节点,“status”将返回贷款批准/拒绝的决定部分。因此,这是我的 REST 等同于GetLoanApprovalDecision的样子:

调用 1

POST http://mybank.loans.com/v2.0/loanapplication HTTP/1.1
<application></application>

对调用 1 的响应返回包含应用程序 ID 的贷款申请资源表示 XML。假设生成的 ID 是 546。在这种情况下,调用 2 将是:

调用 2

GET http://mybank.loans.com/v2.0/loanapplication/546?view=status HTTP/1.1

结论

在下一部分也是最后一篇文章中,我们将涉及实现。我们将创建 REST Web 服务和相应的 REST 客户端——在 .NET 2.0 中。是的,您没听错——.NET 2.0。Microsoft 发布了完全为 SOAP 准备的 .NET 2.0 平台,当他们感觉到对 REST 支持日益增长的需求时,他们开始在 3.0 中添加 REST 支持——而承诺的 3.5 版本无非是 REST!但尽管缺乏支持,您仍然可以在 .NET 2.0 中开发 REST Web 服务。我将在下一篇文章中详细介绍。这将结束这个 REST Web 服务系列。

© . All rights reserved.