
RESTful API Versioning
关于RESTful API Versioning,有很多讨论 Nobody Understands REST or HTTP, Versioning REST Services, best-practices-for-api-versioning, How are REST APIs versioned?, 总结如下。
为什么需要VersioningH1
尽管设计API的时候,我们尽可能设计完美的API,尽可能的避免修改API。但是随着业务需求的变更,API接口的变化几乎是无法避免的。
当业务需求变更的时候,可以这样选择:
- 保持接口的兼容性。这是一种方式,但是并非能切实做到的,为了兼容必定会损失一些新特性,或者牺牲良好的代码为代价。
- 修改API接口的同时,修改客户端。除非自己维护少量的客户端,否则这几乎是不现实的。
- 保留旧的接口,通过版本来实现API接口的变更。
通常情况下,我们会选择第三种方式来实现API接口的变更。
实现策略H1
URI
https://api-v1.example.com/placeshttps://api.example.com/v1/places
上述两种方式都是分别通过Path和Hostname来进行versioning。这种方式的好处直观,友好,易于理解,“复制&粘贴”更为友好;但是RESTful本身就不是“复制&粘贴”友好的。RESTafarian们根本不认同这是RESTful API,因为它破坏了HATEOAS,直接称它为TUK(The URL is King)。
请求体,Query参数 POST/placesHTTP/1.1 Host:api.example.com Content-Type:application/json
bash
{"version" : "1.0" 7}如果客户端的请求是JSON格式的,实现起来倒是不难。如果是Content-Type是image/png 或者是text/csv呢,这就不好处理。
或者把参数放到Query参数里: POST/places?version=1.0HTTP/1.1 这种情况下,如果是POST请求呢,一些框架在POST请求的时候会直接忽略GET参数,虽然这有悖于HTTP协议,但是在POST请求带入GET参数,还是让人非常困惑的。
自定义请求头 GET/placesHTTP/1.1 Host:api.example.com BadApiVersion:1.0
看起来好像挺好的,但是这绝对代码中的坏味道。为了让缓存系统能够正确低返回果,我们Response必须是这个样子的:
bash
HTTP/1.1200OKBadAPIVersion:1.1Vary:BadAPIVersion如果不指定Vary,像varnish缓存系统是不知道如何缓存这样的请求的。另外,抛开这点不说,要知道这个HTTP还必须通过查阅文档才能了解,这也挺恼人的。关于Vary头,请参考6。
Content Negotiation 这种方式是符合HATEOAS,也是相对最优雅的方式。Github API的Versioning就是通过这种方式实现的。
bash
Accept:application/vnd.github.user.v4+jsonAccept:application/vnd.github.user+json;version=4.0这种方式,对HATEOAS和缓存都非常友好。唯一可能有点麻烦的就是实现, 主流的框架都没有处理自动根据请求的Content-Type处理的机制。
代码实现H1
对于URI的实现策略,最直接有效的实现方式就是通过多个代码库实现API的多个版本。部署时候把不同的版本部署到不同的server上。像这样:
- 1.0/master
- 1.0/develop
- 2.0/master
- 2.0/develop
有一个非常好的Git Flow可以参考。
对于Content Negotiation需要同一个代码库的实现,这个是重点。
以Rails为例。 Rails本身有一个很好的Gem叫做versionist实现了这个功能。 为了更好的理解其实现,我们可以自己实现以下。
首先是路由,给v1版本设置namespaceH2
/config/routes.rb
bash
Store::Application.routes.draw donamespace :api donamespace :v1 doresources :productsendendresources :productsroot to: 'products#index'end
接着是v1的实现H2
/app/controllers/api/v1/products_controller.rb
bash
module Apimodule V1class ProductsController < ApplicationControllerrespond_to :jsondef indexrespond_with Product.allenddef showrespond_with Product.find(params[:id])enddef createrespond_with Product.create(params[:product])enddef updaterespond_with Product.update(params[:id], params[:products])enddef destroyrespond_with Product.destroy(params[:id])endendendend
然后,v2版本需要变更这个APIH2
数据库变更H3
/config/db/migrations/201205230000_change_products_released_on.rb
bash
class ChangeProductsReleasedOn < ActiveRecord::Migrationdef uprename_column :products, :released_on, :released_atchange_column :products, :released_at, :datetimeenddef downchange_column :products, :released_at, :daterename_column :products, :released_at, :released_onendend
这样V2版本已经能正确返回的修改后的结果了。
保留V1H3
bash
module Apimodule V1class ProductsController < ApplicationControllerclass Product < ::Productdef as_json(options={})super.merge(released_on: released_at.to_date)endendrespond_to :json# Actions omittedendendend
这里两个版本的ProductsController有很多重复代码,不太符合DRY原则。旧版本的代码的保留通常了为了兼容,总有那么一天旧版本的API废止了,那么重构就不值得了。如果确实觉得需要,可以把共通的行为提取到超类里面。
评论
新的评论
上一篇
基于Grunt的前端项目的完整构建
本文主要简述一个前端项目使用grunt来进行构建的过程。项目本身是基于 RequireJS 的,样式则使用了 Less 来书写。 项目结构 先来看一下项目整体的目录结构。 Gruntfile 通常我们需要grunt分别在开发,以及部署时为我们进行构建。 运行时构建 运行时构建需…
下一篇
对于OAuth2.0授权方式的理解
概述 OAuth2.0的最好的文档,莫过于 RFC 6749 。OAuth2.0本身是一个比较灵活的标准,能够适配应用到各种场景。本文主要是对文档的翻译以及加上部分个人的理解。 角色 OAuth定义了4个角色 resource owner 能够对受保护的资源进行授权的实体。…
