关于 best practices for api versioning 的讨论已经有很多了, Lexical Scope 列举了很多国外知名网站如何实现web api versioning的。
为了支持服务器的多版本API,需要在请求中植入api版本号,添加版本号的方式大体分为两种:
修改url,包括直接修改 url path(api/v1/bookings) 以及添加 query parameter(api/bookings?version=v1) 利用requesr header,例如 Accept , ContentType , 自定义header( X-Api-Version )直接修改url的方式简洁易懂,需要每个接口版本分别定义url,以及相应的view,例如:
# urls.py urlpatterns = [ url(r'^v1/bookings/', v1.BookingsView.as_view()), url(r'^v2/bookings/', v2.BookingView.as_view()), ]这种方式最容易理解和操作,但是会带来很多不便。首先针对客户端,每次接口版本升级,都需要改变url。但url只与获取的资源相关,而不应该与资源以何种类型展示相关! 然后针对服务器自身,每个版本的接口都需要维护一套代码逻辑,导致代码臃肿,可复用性查!
利用request header的方式,个人认为更加灵活,客户端不需要每次改变请求的url,但这就要求服务器能灵活地根据api version路由到正确的处理方法。
HTTP GET: https://example.org/api/bookings X-Api-Version: v2但目前在django中没有遇到一种很好的实现方式可以支持服务器灵活路由, 这位作者 给了一种实现方式,可以在自定义header中传入版本号,并利用url的namespace实现不同版本的路由。 做法是首先利用django middleware拦截请求并将版本号植入url中(其实url还是被改变了,只是对客户端无感),然后还是需要在urlpattern中加一条url,以及定义多套接口实现。所以这种做法的本质还是改变了url,只不过是由服务器自己动手。
API Versioning with django decorator因为公司业务发展,也需要同时支持多个版本的接口,宝宝还是自己动动脑吧~ 于是自己DIY了一套实现方式,所以正文来啦!! 原理是将api version放入自定义header( X-APi-Version )中,然而urls.py中就不要定义多条url匹配规则了,所以写了一个神奇的 decorator ,可以根据 url 和 api version 直接匹配到正确的方法,而且同时支持 View 和普通 method 。下面分两部分讲支持View以及支持普通method的decorator。
decorator for versioning with class-based view给需要管理接口版本的 class-based View 加上 @versioning 装饰器,我们只需要按照下面的方式使用api version。
# urls.py urlpatterns = [ url(r'^bookings$', BookingsView.as_view()), ] # views.py @versioning class BookingView(View): def get(self, request): pass def get_v2(self, request): pass然后发出请求,就能正确路由到 BookingView 的 get_v2 方法了
HTTP GET: https://example.org/api/bookings X-Api-Version: v2重点就在 versioning 装饰器,主要做法就是重写 django View 的 dispatch 方法。 原生的 dispatch 方法是根据 View 名称和 request method(get/post/put/delete) 返回对应的 view class 中和 request method 同名的方法。原理很简单,那我们完全可以改造一下 dispatch 方法的匹配规则,根据 view name 以及 request method 以及 api version 三者在 view class 中匹配方法:
def versioning(origin_class): def dispatch(self, request, *args, **kwargs): methods = [] for i in dir(origin_class): if i.startswith(request.method.lower()) and i != request.method.lower(): methods.append(i) if request.method.lower() in self.http_method_names: # 有版本号时,返回和版本号对应的方法。例如(Get,X-Api-Version:v2)对应view中的get_v2方法 if request.version: handler = getattr(self, get_adaptive_method(request.version.lower(), request.method.lower(), methods), self.http_method_not_allowed) else: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs) # 匹配和当前版本对应的方法,如果没有匹配,则向下兼容找到当前最大版本号的方法 def get_adaptive_method(version, http_method, methods): if http_method + '_' + version in methods: return http_method + '_' + version methods.sort(key=lambda x: int(x[x.index('v')+1:]), reverse=True) for m in methods: if int(version[1:]) >= int(m[m.index('v')+1:]): return m return http_method # 替换django View的dispatch方法 origin_class.dispatch = dispatch return origin_class decorator for versioning with method view然而如果是针对 method view ,就没法重写 dispatch 方法,如何通过 @versioning 路由?
def versioning(origin_object): @wraps(origin_object) def wrapper(*args, **kwargs): version = args[0].version if not version: return origin_object(*args, **kwargs) else: module = origin_object.__module__ module = import_module(module) members = dir(module) # 获取origin_object所在module中以方法名开头的所有方法 methods = [member for member in members if member.startswith(origin_object.__name__) and member != origin_object.__name__] # 同上,已向下最大匹配原则匹配正确版本号的方法 version_func = get_adaptive_method(version, origin_object.__name__, methods) if version_func != origin_object.__name__: func_call = getattr(module, version_func) return func_call(*args, **kwargs) else: return origin_object(*args, **kwargs) return wrapper同样也是一个 decorator 搞定,和 class-based view 的 @versioning 原理类似,根据 view name 和 api version 匹配模块中的方法。区别就是 class-based view 的 @versioning 只需要在 view class 中匹配方法,而 method view 没有类似class的局部作用域,所以需要在其所在模块中匹配正确方法。
所以具体用法就是:
# urls.py urlpatterns = [ url(r'^bookings$', book_list), ] # views.py @versioning def book_list(request): pass def book_list_v2(request): pass这里 urlpattern 中和 url 绑定关系的是 book_list 方法,所以只需要在 book_list 方法前加上 @versioning 装饰器,而且如果该方法还有其他装饰器,记得将 @versionging 放在最上面,保证在路由到正确方法以前不执行任何逻辑操作。
因为view.py中会同时涉及到 class-based view 以及 method view ,所以将上面两种写法合并为一个统一的装饰器,只需要根据被装饰的对象的类型判断到底执行哪套decorator。
# return method view decorator if isfunction(origin_object): return wrapper # return class-based view decorator if issubclass(origin_object, View): origin_object.dispatch = dispatch return origin_object 总结django-rest-framework 中也提供了api version的方案,但也没有给出具体的实现机制。目前也没有一个公认的Api Versioning最佳实践方式,许多大型互联网公司的版本管理方案都不同,所以只能根据业务本身特点以及个人喜好制定不同的方案。
Versioning an interface is just a “polite” way to kill deployed clients. Roy Fielding
API Versioning 是为了兼容历史版本,但对历史版本的API支持一定要有时间和用户限制,如果投入过多精力维护n个版本以前的接口,只会让代码越来越臃肿难看,开发人员痛苦不堪。