使用Raml构建Restful API

为了更好地阅读体验,欢迎访问博客原文

最近加入了一个颇具挑战性的项目(微服务架构+前后端分离部署+容器化部署+单点登录+多系统集成等)。从微服务容器化这两个词就能看出,我们Team是比较赶时髦的(PS:当然赶时髦的核心还是业务驱动)。既然是微服务,就避免不了存在众多服务之间的交互,除此之外,前后端分离的架构也促使了我们Team毫不犹豫地选择了RESTFul风格的API。

本文重点介绍如何使用Raml来构建RESTful API,从而来提高微服务团队开发人员的合作,涵盖了以下内容:

1. 恰到好处的Raml。
2. 十分钟快速上手。
3. 搭建Mock Server。
4. 使用Docker分离数据和构建过程。

本文假设读者对RESTful API有一定的基础了解,关于RESTful API,推荐阅读 阮一峰 的 RESTful API 设计指南


恰到好处的Raml

在前后端分离的架构中,一个经典的优秀实践是CDCD(Consumer Driven Contract Development),即消费者驱动合同开发。该实践的两个核心点是:消费者驱动 + 合同

1. 消费者驱动。Service所提供的API是给不同的消费者去使用的,所以消费端需要消费什么数据,API就应该返回什么数据。剩下就是合同了,通俗的将就是API契约,
2. 合同。通俗的讲是一个API契约,Service和Consumer(Consumer可以是前端或另一个Service)的API结构需要一个契约来统一管理,这样一来,负责Service和Consumer的开发人员都能参照同一套API契约独立并行开发,双方完成后进行集成联调。

优秀的实践(CDCD)总是离不开优秀工具来做支撑,Raml 以及其生态群恰到好处地提供了我们想要的合同功能。Raml 是什么,先看一张官网:

使用Raml构建Restful API_第1张图片

DesignBuildTestDocumentShare,我们选择 Raml ,究根结底也是因为它这几大优秀的特性:

1. 设计API。你可以快速的构造你的API,并以人类友好的格式将它呈现出来。它涵盖了一些重要设计的最佳实践,如建模、模式、模板以及代码重用。
2. 构建API。一旦设计好你的API,你就可以借助一些开发工具,将设计好的静态API文档,变成一个服务器端来提供服务。
3. 测试API。引入单元测试可以有效地保证API实现的正确性,你可以通过运行一些脚本来测试你服务端是否涵盖了你设计好的API。
4. 文档化API。Raml可以帮助你脱离同步维护一份额外文档的痛苦。RAML是一门API描述语言,所以你的API一旦被描述,它就是一份现成的API文档。你可以借助一些工具将它生成可视化的文档。
5. 分享以及维护你的API。你可以借助一个基本的JavaScript来生成一些交互式工具(API Consoles或API Nodebooks),这样其他开发者可以使用标准格式与你的维护团队进行交流。

接下来,我们主要针对 Raml 的前四个特性进行一个实践尝试。


窥探Raml

共享俨然成为了一个非常时髦的词汇。共享单车、共享汽车这些产品雨后春笋般的兴起。它们有一个共性:商家集中提供大规模的服务,用户可以付出极低的价格就可以享用这种服务。不可否认,这些产品正在促进社会进步(绿色出行),而真的共享了吗?我是持有怀疑态度的...

我们要开发一款产品共享图书馆,一句话描述该产品的理念:

任何注册用户都可以将自己拥有的图书共享到一个集中的虚拟图书馆,同时可以从这个图书馆中借阅任何图书。

产品立项进入了开发期,第一个迭代(敏捷开发的术语,关于敏捷开发推荐阅读 我在ThoughtWorks的敏捷实践),团队目标是用户管理的功能。我们将以用户故事的形式展开展开:

作为管理员,ta可以查阅用户列表:

/users:
  get:
  description: 查看用户列表
  queryParameters:
    gender:
      description: "性别"
      required: false
      type: string
      pattern: "MALE|FEMALE"
  responses:
    200:
      body:
        application/json:
          example: |
            [
              {
                "id": "2nldksfr4f2ifoa4g43rvfsdfdsfdaf2",
                "account": "sjyuan",
                "phoneNumber": "18192235667",
                "gender": "MALE",
                "name": "袁慎建"
              }
            ]

该API表示:操作资源/usersget表示使用的是HTTP GET方法。queryParameters定义了查询参数,responses定义了返回值,example描述了response body,它是一个JSON数组。

作为普通用户,ta首先要注册一个账号:

/users:
  post:
  description: 注册一个新用户
  body:
    application/json:
      schema: |
        {
          "type": "object",
          "$schema": "http://json-schema.org/draft-03/schema",
          "required": true,
          "properties": {
            "account": {
              "type": "string",
              "required": true,
              "minLength": 2,
              "maxLength": 20
            },
            "password": {
              "type": "string",
              "required": true,
              "minLength": 6,
              "maxLength": 32
            },
            "phoneNumber": {
              "type": "string",
              "required": true
            },
            "name": {
              "type": "string",
              "required": false
            }
          }
        }
  responses:
    201:
      body:
        application/json:
          example: |
            {
              "id": "2nldksfr4f2ifoa4g43rvfsdfdsfdaf2",
              "account": "sjyuan",
              "phoneNumber": "18192235667",
              "name": "袁慎建"
            }

该API使用了HTTP POST请求,schema定义了request body的校验格式。

作为普通用户,ta可以查看自己的详情:

/users:
  /{account}:
    get:
    description: 查看给定账户名的用户信息
    responses:
      200:
        body:
          application/json:
            example: |
              [
                {
                  "id": "2nldksfr4f2ifoa4g43rvfsdfdsfdaf2",
                  "account": "sjyuan",
                  "phoneNumber": "18192235667",
                  "gender": "MALE",
                  "name": "袁慎建"
                }
              ]

/{account}是一个URI参数,传入的是用户的账号。

作为普通用户,ta可以编辑自己的信息:

/users:
  /{account}:
    put:
    description: 更新给定账户名的用户信息
    body:
      application/json:
        schema: |
          {
            "type": "object",
            "$schema": "http://json-schema.org/draft-03/schema",
            "required": true,
            "properties": {
              "password": {
                "type": "string",
                "required": true,
                "minLength": 6,
                "maxLength": 32
              },
              "name": {
                "type": "string",
                "required": false
              }
            }
          }
    responses:
      301:

更新一个用户信息,我们使用了HTTP POST 请求。

作为一个管理员,ta可以禁用或删除某些违规操作的用户:

/users:
  /{account}:
    delete:
      description: 删除给定账户名的用户
      responses:
        204:

删除一个用户,我们使用了HTTP DELETE请求。

到此为止,我们完成了简单的User增删改查的API设计。我们只是做一个快速的窥探,真实业务场景中的API远比这要复杂得多,我们会利用include来提高代码的复用,而一些Raml的高级特性,诸如:data typeseurityResource TypesTraits等,也将成为强有力的工具。关于Raml更多详细指南,可参阅: Raml-spec。

完整源代码保存在我的GitHub的 restful-api-raml。


构建你的API

我们已经完成了API的设计(用户的增删改查),API契约(合同)定好了。前、后端开发人员开始进入开发阶段。

接下来有新的需求:

1. API设计好了,如何将其文档化,提供用户有好的阅读格式呢?
2. API设计好,如何搭建基于设计API的Web Server,给前端的集成测试提供支持呢?

问题一是前文提到的Raml的一大特性:文档化API,问题二就需要我们将设计好API文档转变成一个Web Server,它也是Raml一大特性的体现:构建API。我们需要借助一些工具来完成这两件事情。


文档化API

在Raml官方文档中提及了三种工具,API Console、RAML to HTML、RAML2HTML for PHP,我们将使用 RAML to HTML去做Raml到html的转换,再利用 Live Server run起一个静态文件服务,提供在浏览器查阅API文档的功能。

在开始之前,我们需要安装 npm。使用npm下载依赖,并定义一个任务:

{
  ...
  "dependencies": {
    "raml2html": "^6.1.0",
  }
  "scripts": {
    "docs-generator": "raml2html data/api.raml > api.html"
    "docs-server": "live-server --port=8081 --watch=api.html --entry-file=api.html"
  }
  ...
}

docs-generator 任务是用来将raml文档转换成html文档,docs-server 启动了一个轻量的静态文件服务,它可以监听api.html文件的变化(只需要刷新浏览器),运行下面命令,之后访问http://127.0.0.1:8081之后,可以看到:

$ npm run docs-generator
$ npm run docs-server
使用Raml构建Restful API_第2张图片

构建API

设计好了User模块的API,也完成了其文档化、可视化的工作,接下来我们要把API塞入到一个Web Server中,供前端开发人员在代码中调用(User的增删改查)了。

在 Raml官方文档中 Build Your API 推荐了NodeJSJavaPython等家族的工具。为了保持一致性,我们会采用NodeJS族的 Osprey。这里我们使用了 osprey-mock-service,它只是对 Osprey进行了一层包装。

同样,我们需要安装osprey-mock-service依赖,并定一个任务:

{
  ...
  "dependencies": {
    "osprey-mock-service": "^0.2.0"
  }
  
  "scripts": {
    "mock-server": "osprey-mock-service -f data/api.raml -p 8080 --cors"
  }
  ...
}

运行mock server,测试添加用户的API:

$ npm run mock-server 
> [email protected] mock-server /Users/sjyuan/Personal-sjyuan/ysj_hub/dojo/dojo-springboot/springboot-mongo/dojo-restful-api-raml
> osprey-mock-service -f data/api.raml -p 8080 --cors

Mock service running at http://localhost:8080


$ curl -H "Content-Type: application/json" -X POST -d '{"name":"袁慎建","password":"sjyuan@2017", "phoneNumber":"18192235667", "account":"sjyuan"}' http://localhost:8080/v1/users

{"id":"2nldksfr4f2ifoa4g43rvfsdfdsfdaf2","account":"sjyuan","phoneNumber":"18192235667","gender":"MALE","name":"袁慎建"}

构建与数据分离

目前为止,我们已经完成了API的文档化与构建,构建行为借助了raml生态群中的几款NodeJS插件以及npm工具,这意味着我们如果在自己的机器中构建API必须安装npm以及相关依赖(可能还会牵扯版本的问题)。而对于一个纯粹的Java、Python或C#开发者来说,他们来说是不友好的。为了解决这个问题,我们利用当下流行的容器技术来实现构建行为和数据进行分离,将依赖和构建都封装起来,将使用人员从繁琐的依赖中解脱出来。

Docker可以为我们提供良好的容器封装隔离,让我们来看看封装了构建行为的docker file:

FROM node:7-alpine
MAINTAINER "Live platform Team" 

RUN apk --update add git supervisor && rm -rf /var/cached/apk/*

# Prepare work directory and copy all files
RUN mkdir /app
WORKDIR /app

# install dependencies
COPY package.json /app
RUN cd /app && npm i --silent

COPY supervisord.conf /app
COPY watcher-tasks.js /app
COPY supervisord.conf supervisord.conf

EXPOSE 8080 8081
CMD ["/usr/bin/supervisord"]

supervisord.conf是 supervisord 的配置文件,用于管理进程。watcher-tasks.js是自定义的一个JS文件,用于执行npm任务。


为了简单起见,我们使用 docker-compose 来启动容器:

version: '3'
services:
  raml:
    build: .
    ports:
      - "8088:8080"
      - "8082:8081"
    volumes:
      - ./data:/app/data

docker-compose.ymal文件定义了从当前目录中的Dockerfile去构建镜像,通过映射将我们业务api目录挂载到容器内部。


做了这些工作,我们可以使用docker-compose来启动容器:

$ docker-compose up

之前我们通过8081和8080来访问API文档和Mock server,使用容器之后,端口分别映射成了8082、8088。

实践指导:
可以将构建的镜像上传到一个私有的镜像仓库或者 Dockerhub 中,在使用的地方将镜像源指向你的镜像所在的仓库即可。


总结

如今微服务架构模式越发流行,REStful API也充斥在各个开发团队中。Raml的为我们提供了一个很好的API契约管理工具,借助一些插件以及流行的容器化技术(Docker),我们就能够让它发挥更大的价值。

本文所涉及的源代码被host在 restful-api-raml。中,欢迎Clone或Fork。

你可能感兴趣的:(使用Raml构建Restful API)