业界目前已经在以下这一点上达成了强烈的共识:API的设计应该是按照前端到后端的次序进行的,将重点放在开发者对API的使用上。在某个发展兴旺的商业体中,通常会存在着许多互相竞争的产品与API的实现,而易于使用、并且设计良好的API在吸引与保留开发者这一点更有优势。此外,新的工具不断涌现,它们不仅能够帮助开发者按照迭代方式设计API,而且能够帮助开发者学习与使用这些API。
API设计中的关键因素在于“人”,这意味着API设计工具与文档格式必须便于人的读写,这一段时期以来的API设计标准都在强调“以人为本”。API Blueprint在这一领域是较早使用Markdown作为格式的产品,而进行API设计的广大开发者与商业分析人士都熟悉这种格式。
去年晚些时候,Mulesoft发布了自己的RESTful API建模语言——RAML。这门语言本身只是一种具有专利的第三方语言,但由于它的一系列特色,使得广大的API社区都对它产生了浓厚兴趣:
RAML本身不会带来良好的API设计,这门语言及其工具也没有任何内置的“最佳实践”。正如MuleSoft的CTO Uri Sarid所说,RAML的目标是:“以一种开放的、简单且简洁的规格对API进行描述,它能够捕捉到API的结构,并鼓励基于模式的设计和重用,它将有助于释放API的商业潜力。”
RAML并不负责API的实现,它仅仅是一门规格说明语言。现在仅有为数不多的一些开源工具能够实现由RAML语言所描述的API。APIKit能够将一个RAML规格转变为Mule ESB流,如果你的运行时平台使用的是Mule ESB,那这一工具能起到些作用。JAX-RS Codegen的使用更加广泛,因为它能够将一个RAML规格转变为JAX-RS Java代码。毫无疑问,对其它语言和平台的支持,将在很大程度上决定RAML最终是否能够取得成功。
本文的目的是以一个示例为你展示如何使用RAML设计一个简单的,但又实用的API,并展现RAML的一些选择性。而不是作为与其它API设计或描述格式进行比较的依据,例如API Blueprint、IO Docs或Swagger。它令人振奋的地方在于,我们现在设计的选择上有了一种可行并且有竞争力的生态系统,它有希望建立起RESTful API设计与规格的共识。
我们将要设计的这个示例API的功能是将事件日志记录到一个流中,以支持在线分析与报表业务。使用日志文件保存正在运行的软件的状态与进度,这种模式大概与计算机本身历史一样长。但随着系统变得越来越分布式,负载也被分到大量系统中(或许只是短暂的发生),使用文件来管理及分析日志成为了一种不可能的任务。目前流行的设计模式是将日志看作为事件流。这样一来,这个Eventlog API就包括了以下一些基本需求:
现在让我们开始对RAML的探索,看看如何使用它描述一个满足以上需求的API。
在本文中,我们将使用Anypoint API设计器编写RAML。这个设计器允许我们在设计过程中查看可交互的文档,并且能够对API进行模拟(mock)。我们将从一个简单的规格开始,随后再使用RAML中更高级的特性,以使我们的规格更加DRY。REST API的规格有时重复性很高,包含大量的样板代码,而且对于非常接近的资源信息也不得不重复编写它们的规格。RAML提供的特性能够帮助你避免或是管理这种重复性,包括资源类型(resourceType)、trait和文件中包含的指令。我们会在过程中逐步讲解这些特性,在示例的最后将讲述安全性规格。
你可以在raml.org网站上下载到RAML的文档与入门指南,包括完整的RAML规格说明。在这个示例的开发中,我使用了Anypoint API设计器,它包含了一个在线的RAML编辑器。
首先,这个事件日志API包含了两种主要的资源:一个事件集合,我们将其称为一个事件流;以及每个独立的事件对象自身。对于事件流来说,我们需要一个POST方法,在事件流中创建一个新的事件,还需要一个GET方法以返回流中的事件。可以通过事件流名称和事件Id获取每个独立的事件。下面这段代码片段展示了这个规格的相应RAML代码:
#%RAML 0.8 # Basic starter raml with two resources and corresponding methods. title: Eventlog API version: 1.0 baseUri: http://eventlog.example.org/{version} /streams/{streamName}: displayName: A Named Stream description: A stream is a collection of related events. A Named Stream has been defined by the user to contain a list of related events. Named Streams are created by POSTing an event to a Named Stream. get: description: Get a list of events in this stream. post: description: Create a new event in this stream. /streams/{streamName}/{eventId}: get: description: Get a particular event by its Id.
baseUri属性包括域名和一个版本号,大括号中所指定的是Uri参数。我们要处理的资料集合是基于“/stream/{streamName}”这个地址的一个相对路径,对这个路径调用GET或POST http方法,会对这个集合进行相应的操作。而事件资源是基于根路径“/streams/{streamName}/{eventId}”的一个相对路径,对应一个单独的GET方法。RAML允许我们为这些资源和方法提供一个便于人类阅读的描述信息。API设计器会将这个规格说明以一种可交互的文档形式进行展现,如图1所示:
这个API设计器允许我们使用一个模拟的服务来测试这个API。作为一个简单的例子,我们可以通过指定事件流名称,并单击“POST”按钮以创建一个事件。在默认情况下,你会得到一个HTTP 200响应。
默认的POST方法并不足以完全表现出我们的API所必需的行为与结构。我们必须告知开发者事件对象的对应数据结构,以及在创建时应发生的行为。RAML提供的两个补充选项能够描述这些信息:即schema和示例。
虽然许多的新的公共API倾向于使用JSON表达方式,但也有很大一部分API会同时提供XML,或仅仅提供XML格式。因此RAML可以以一种可编程的方式将规格说明转化为XML或JSON schema。可以选择在一个资源描述文件的内部或RAML头文件的内部指定schema,也可以通过一个外部文件或url进行指定。由于schema内容可能会非常庞大,因此通常来说将它定义在外部是一种良好的实践,因为内联的schema会干扰对使用者对整个RAML规格的理解。
#%RAML 0.8 title: Eventlog API version: 1.0 baseUri: http://eventlog.example.org/{version} schemas: - eventJson: !include eventSchema.json eventListJson: !include eventlistSchema.json
在该示例中定义了一个由外部文件引用的JSON schema描述,RAML可以包含本地文件或基于HTTP的资源。我们选择的方式是通过API设计器指定了几个本地文件。如果这时选择了基于HTTP的资源,会产生了一个跨站脚本错误,因为脚本编辑器不能够引用来自其它域名的资源。在文件的最上方是一个schema标签,可以在之后的资源定义中引用这个标签名称。
有许多API选择用一个示例,而不是schema来表述资源信息,因此RAML也允许你在schema中提供一个示例。下面的代码片段就包含了这样一个示例标签,所谓示例就是一段JSON或XML代码,它表述了该资源体的内容。
当开发者向事件流中POST了一个新事件后,他们应当收到一个信息,表示该事件资源已经被创建,并且收到这个新事件的url(其中包含了Id信息)。服务端将返回一个HTTP 201响应(已创建),并且在HTTP头中包含一个属性“location”,指向新资源的url。下面的示例为我们展示了在向事件流中POST一个新事件时,应该怎样指定请求与响应信息。
/streams/{streamName}: displayName: A Named Stream description: A stream is a collection of related events. A Named Stream has been defined by the user to contain a list of related events. Named Streams are created by POSTing an event to a Named Stream. post: description: Create a new event in this stream. body: application/json: example: | { "name": "Temperature Measurement", "source": "Thermometer", "sourceTime": "2014-01-01T16:56:54+11:00", "entityRef": "http://example.org/thermometers/99981", "context": { "value": "37.7", "units": "Celsius" } } responses: 201: description: A new event was created. headers: location: description: "Relative URL of the created event." type: string required: true example: /streams/temperature/123456 body: null
这段代码示例的作用是向一个名为“temperature”(天气)的事件流中POST一个天气预测事件的RAML描述。其中描述了JSON对象的结构,并提供了一个示例。事件包含了一系列你所需的属性,例如它的名称(或类型)、来源和时间戳。我们还加入了一个可选的引用对象,它指向某个与该事件相关的信息实体(entity ref),以及一个用来指定事件类型(在这个例子中是指value和units)的上下文数据(context data)。
在响应的规格中,我们详细描述了这个201响应的相关信息。当然,规格中或许可以包括400或500等范围的响应信息,用以表示的发生的问题,不过现在就使用这个默认的设定就可以了。这个API的重要之处在于,开发者应该收到一个201(已创建)响应,并且在它的头信息中应该有一个包含了新创建事件的相对路径的属性。我们还指定了响应体的内容为空。如果开发者需要再次获取这个事件,那他们就可以使用这个相对url进行访问。
对该事件集合执行GET方法,要么返回一个404响应(如果该事件流尚未创建),要么返回一个200响应,并在响应体中包含了一个事件数组。请看以下代码:
get: description: Get a list of events in this stream. responses: 404: description: The specified stream could not be found. 200: description: Returns a list of events. body: application/json: example: | [ { "id":"123456", "name": "Temperature Measurement", "source": "Thermometer", "sourceTime": "2014-01-01T16:53:54+11:00", "entityRef": "http://example.org/thermometers/99981", "context": { "value": "37.1", "units": "Celsius" } } { "id":"123457", "name": "Temperature Measurement", "source": "Thermometer", "sourceTime": "2014-01-01T16:54:54+11:00", "entityRef": "http://example.org/thermometers/99981", "context": { "value": "37.3", "units": "Celsius" } } { "id":"123458", "name": "Temperature Measurement", "source": "Thermometer", "sourceTime": "2014-01-01T16:55:54+11:00", "entityRef": "http://example.org/thermometers/99981", "context": { "value": "37.5", "units": "Celsius" } } ]
尽管我们才刚开始编写这个API规格,但它已经显得非常臃肿了。大量的资源,以及它们的方法、参数和示例造成了该规格说明极大的重复性,并且有可能因此产生bug。但RAML提供了一系列特性,它能帮助我们避免这种重复性。
对于集合来说,一个POST方法会得到一个201响应,其中包含了资源的地址;并且一个GET方法会返回一个资源列表,这已经成为一种通用的模式了。因此我们可以将这种模式抽象出来,建立一个resourceType规格,而其它资源对象都可以从它进行继承。
resourceTypes: - collection: post: responses: 201: headers: location: description: The relative URL of the created resource. type: string required: true example: /streams/temperatures/12345 get:
这种方式极大地简化了事件流资源的描述,它允许我们只需指定事件流中特定的部分就可以了,例如示例中的请求体与响应体。请特别留意在资源定义中的type关键字。
/streams/{streamName}: type: collection displayName: A Named Stream description: A stream is a collection of related events. A Named Stream has been defined by the user to contain a list of related events. Named Streams are created by POSTing an Event to a Named Stream. post: description: Create a new event in the stream body: application/json: example: | { "name": "Temperature Measurement", "source": "Therometer", "sourceTime": "2014-01-01T16:56:54+11:00", "entityRef": "http://example.org/thermometers/99981", "context": { "value": "37.7", "units": "Celsius" } } get: description: Get a list of events in the stream. responses: 200: body: application/json: example: | [ { "id":"123456", "name": "Temperature Measurement", ... etc ...
在我们当前已实现的API规格中,还遗漏了一项重要的特性,即针对各种标准对所请求的事件进行过滤的功能。我们的需求中包括这些内容:根据时间段获取事件、或者获取一个分页的事件列表,对返回的事件数量加以限制,以避免服务 端或客户端的数据量过载。我们可以通过指定trait的方式为所有资源实现这一需求:时间段是一个trait,分页是另一个trait,而数量限制又是一个trait。resourceTypes为资源规格提供了继承的能力,而trait则负责处理横切关注点,通常情况下在指定查询参数时会遇到这种问题(但不仅限于这种情况)。
下面一段代码描述了三种trait:Slidingwindow指定了两个查询参数,它代表了相对于当前时间的一个时间窗口。Windowstart则代表了当前时间与该时间窗口开始时间的间隔,通过一个整数与一个单位符号的方式表示。例如:“1h”表示一个小时,“30m”表示三十分钟,而“86164s”则是以秒为单位来表示一整天。Windowsize是另一种时间间隔(使用相同的表达方式),它表示该时间段的长度。
paginated这个trait表示通用的查询参数,每次都返回一个页大小的查询数据,pagenumber和pagesize参数对应于当前页面和页面大小。
最后,limited trait指定了一个通用的行数限制,这是为了防止某个缺乏经验的客户端程序试图返回事件流中的所有事件,并影响到整个系统的运作。默认的限制大小是100,也可以在任意请求中覆盖这个值。
traits: - slidingwindow: description: Query parameters related to retrieving a sliding window of timestamped entities relative to now. queryParameters: windowstart: description: The begining of the sliding window expressed as a time interval from now represented as an integer concatenated with units h (hours), m (minutes), s (seconds) or ms (milliseconds). type: string example: 1h, 30m, 3600s windowsize: description: The end of the sliding window expressed as a time interval from the start as a concatenated integer and unit suffix. type: string example: 10s, 1h, 25m - paginated: pagenumber: description: The page number of the result-set to return. type: integer minimum: 0 pagesize: description: The number of rows in a page request. type: integer maximum: 100 - limited: queryParameters: limit: description: A general limit on the number of rows to return in any request. type: integer default: 100
事件流资源规格通过is关键字,以一个YAML数组的方式列举所有相应的trait,并将这些trait应用到GET方法中:
/streams/{streamName}: type: collection ... etc ... get: is: [ slidingwindow, paginated, limited ]
在应用了resourceType和trait的改动之后,我们可以在API设计器中重新审阅一下这些API,观察所生成的文档,并利用模拟服务尝试调用API。图2展示了调用的结果,包含了完整的查询参数文档。所生成的文档同时指定了事件流资源的resourceType以及与事件流资源的GET方法相关的所有 trait。
对于API设计者来说,API的安全性是最高优先级的任务,并且要不断地兼容各种协议与标准。幸运的是,RAML为广泛的安全性schema提供了支持,包括OAuth 2.0、OAuth 2.0、基本验证(Basic Authentication)和摘要验证(Digest Authentication),另外还可以应用自定义schema。所有这些schema都必须提供头、查询参数和响应的定义。在这个示例中,我们将使用OAuth 2.0,但只将其应用到事件的POST方法。任何人都可以读取事件流,但只有授权客户才能够POST事件。
以下代码描述了一个OAuth 2.0安全性schema,它由一个HTTP头字段Authorizaton指定,并且只使用客户端凭证授权。RAML规格中还描述了各种安全性schema的不同使用方式。
securitySchemes: - oauth_2: description: Eventlog uses OAuth2 security scheme only. type: OAuth 2.0 describedBy: headers: Authorization: type: string description: A valid OAuth 2 access token. responses: 401: description: Bad or expired token. 403: description: Bad OAuth request. settings: authorizationUri: http://eventlog.example.org/oauth2/authorize accessTokenUri: http://eventlog.example.org/oauth2/token authorizationGrants: [ credentials ]
oauth_2安全性schema是通过在事件流POST方法中指定securedBy关键字的方式得以应用的。
/streams/{streamName}: post: securedBy: [ oauth_2 ]
注意,这个securedBy关键字接受一个数组,它允许指定将多种安全性schema应用到API上,或者是应用到单独的资源上。
我们使用了一个非常简单的API简要地介绍了一个RAML规格的各个方面,目的是为了强调RAML的特性对API描述方面在代码重用和DRY原则方面所起的作用,包括以下关键因素:
最后,RAML提供了一种可以描述广泛的安全性schema的机制,包括当今我们通常使用的schema。
本文所描述的RAML的完整示例可以在Github上找到:github.com/scaganoff/example-eventlog-api。
Saul Caganoff是一家澳大利亚的系统整合顾问公司Sixtree的CTO,他在系统整合和软件项目开发上具有大量的架构师和工程师的经验,所参与的项目横跨澳大利亚、美国和亚洲。Saul的专业爱好包括所有级别的架构,包括企业架构和解决方案架构,以及各种类型的应用程序,包括分布式系统、组合应用、云计算和云API。
查看英文原文:Designing an Event Log API with RAML