为什么需要API多版本开发?
如果把云服务看做产品,那么API就是这个产品面向顾客的一种最基本的形态。产品需要逐渐演进,API也就不可避免的出现参数、返回值等的变化。如果是定制产品,我们也许还可以和顾客一起协商修改API的调用方式,但现在,我们面向的是多样化,不固定的顾客,我们不可能要求每一个顾客都能按我们的要求,在指定的时间内修改调用方式。这就促使我们探索一套多版本API的开发方式。
采用哪种形式区分不同版本?
1 Header版本控制
此方法需要客户端将指示资源版本的自定义Header添加到请求中,如果省略了此Header,按默认值(一般是最新版)处理。1
2
3
4
5
6
7
8
9# Request
GET http://adventure-works.com/customers/3
Custom-Header: api-version=1
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
优点:纯粹的版本控制机制,符合 RESTful API「每个资源使用唯一的URI定位」的原则
缺点:不直观,无法支持表单直接调用(众所周知HTML表单是不能添加自定义Header的),缓存不友好(不能认为同一URI/查询字符串指向的是相同数据,因此难以缓存)
2 媒体类型版本控制
通常,Accept标头的用途是由客户端指定响应的正文是 XML、JSON或其他格式。但是,可以定义包括以下信息的自定义媒体类型:该信息使客户端应用程序可以指示它所需的资源版本。1
2
3# Request
GET http://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json
服务端负责处理 Accept 标头并尽可能采用该值(可以在 Accept 标头中指定多种格式,在这种情况下,服务端在其中选择最适合的格式用于响应正文)。 返回结果中的 Content-Type 标头确认响应正文中的数据格式:1
2# Response
Content-Type: application/vnd.company.myapp-v3+xml
优缺点同上
3 URI 版本控制
1 | # Request |
优点:直观,缓存友好,支持表单直接调用
缺点:不符合「每个资源使用唯一的URI定位」的原则
4 查询字符串版本控制
1 | # Request |
优点:直观,缓存友好,支持表单直接调用,符合「每个资源使用唯一的URI定位」的原则
缺点:某些较旧的Web浏览器和代理不会缓存在URI中包含字符串的请求的响应,这会对性能产生影响
综合考虑以上4种方式的优缺点,不难看出,URI和查询字符串这两种方式应该是我们优先考虑的方式,因为:直观,缓存友好,支持表单直接调用在实际应用中是最重要的因素。
至于「每个资源使用唯一的URI定位」的原则……抱歉,现实世界就是有许多不合理但普遍存在的事情,况且,谁说RESTful就一定是对的呢?
后台代码如何组织?
保持向后兼容通常是成本高昂的或非常困难,因此,首先明确一个原则:同时维护尽可能少的版本,通常不超过5个。而实际上,提前规划可扩展性、严格定义的API才是更重要的。
同时,如果旧的业务不再建议适用,我们应该尽早地用重定向、文档或者其他机制,通知客户弃用旧版API。
另外,API版本号应该适用一个简单的序号,不要使用像v1.2这样的点符号,因为它意味着版本控制的粒度不够好——这是一个接口,而不是一个实现。
根据PangPangSDK的API多版本演讲开发过程,我认为:后台代码应该采取不断演进的代码组织方式
过程一:项目刚开始的时候,API应该定义为v1版本,但后台代码完全不用区分版本,因为:90%的项目走不到v2版本,就迎来了生命的尽头
过程二:项目演进到一定阶段,由于后台结构、业务细节的变化,某些API的参数和返回值不得不进行修改,此时,我们可以将原有API的Controller、Module放到另外的,以Deprecate_前缀为标识的源文件中,如下图
过程三:当版本超过2个,我们就不能简单的建立deprecate_
类和方法来区分版本了,我们需要对每个版本建立对于的文件夹(如果是go语言,这将出现不同的package,比如cart_v1
、cart_v2
两个package),如下图
在开发过程中,我建议:不要同时修改新旧版本的代码,而应该先开发新版,然后通过中间层的方式,将旧版数据转换成新版可兼容的数据。因为优先保障最新的业务和数据对于一个产品向前发展更有意义,而同时两套后台逻辑既割裂了后台源码的完整性,又很可能拖慢了项目开发的进度。
扩展:SDK如何处理不建议使用的接口
在上文中我使用deprecate这个单词其实是有原因的:我注意到,在原生iOS和Android开发中,当使用一个Library中的类、方法、接口时,可能会出现XXX is deprecated, use XXX
的提示,如下图
和Web API类似,第三方包也会有升级类、方法、接口的时候,为了向下兼容,不得不继续提供旧的调用方式,但应在IDE中显示警告信息,引导开发者使用新的调用方式。而SDK,作为一种第三方包的集合形式,也应该具备这样的特性。
参考资料:
https://stackoverflow.com/questions/389169/best-practices-for-api-versioning/6750376#6750376
https://apigee.com/about/blog/technology/restful-api-design-how-many-versions
https://docs.microsoft.com/zh-cn/azure/architecture/best-practices/api-design