大家是怎么做APP接口的版本控制的?欢迎进来看看我的方案。升级版的Versioning
背景
APP不同于网站,网站程序一发版,所有用户看到的都是最新的页面、调用最新的接口,没有新老版本一说。APP一旦下载到用户手机上,用户不更新你拿他一点办法都没有,但是随着业务的调整,同一个接口的请求参数和输出JSON有变化的话,就需要考虑老版本的兼容问题了。
举个例子:某APP的1.0.0版,服务端用户信息接口(api/User/UserInfo)输出的JSON如下
{
"status": ,
"message": "ok",
"data": {
"userId": ,
"userNickName": "oppoic",
"userName": "汪杰",
"userEmail": "oppoic@163.com",
"userBlog": "https://www.cnblogs.com/oppoic/",
"userPic": "https://pic.cnblogs.com/avatar/401362/20180415232220.png"
}
}
1.0.0版上线之后,产品迭代增加需求:一级菜单加一个付费用户入口,APP一打开就要动态判断当前用户是否付费。几个研发一商量,把用户是否vip的状态放到用户信息接口最合适。修改后服务端用户信息接口(api/User/UserInfo)输出如下
{
"status": ,
"message": "ok",
"data": {
"user": {
"id": ,
"nickName": "oppoic",
"name": "汪杰",
"email": "oppoic@163.com",
"blog": "https://www.cnblogs.com/oppoic/",
"pic": "https://pic.cnblogs.com/avatar/401362/20180415232220.png"
},
"vip": true
}
}
修改前后的JSON有如下变化:
1)增加了是否vip字段;
2)为了结构更清晰,原来的userId、userName、userPic等属性移到到user对象下;
接口改好了之后APP端通过data.vip就可以判断当前用户是否vip了,同时也把原来data.userName改成了data.user.name等等。都改好后升了一个版本号,由原来的1.0.0改成了1.0.1发到了各大商店。
但是一上线就出问题了:升级1.0.1的用户打开个人信息页面正常,但是未升级的1.0.0用户打开个人信息页面报错
VM211:1 Uncaught TypeError: Cannot read property 'name' of undefined
at <anonymous>:1:16
原因是服务端个人信息接口的输出JSON改了,只能适配1.0.1的新版APP,无法满足老用户了。
有人说需求变了我就改服务端接口名称,老用户调老接口,新用户调新接口。这个方法是可以,但是接口多了简直就是一场灾难,项目从此无法维护。
路由方案
通过配置路由来区分不同版本的接口
config.Routes.MapHttpRoute(
name: "DefaultApiWithVersion",
routeTemplate: "api/v{version}/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
APP端请求:api/v1/User/UserInfo,这种方案缺点如下:
1)版本号通常都是三段式的,例:1.0.1,硬套这个的话,服务端要建个命名空间带“.”的文件,C#对“.”支持不太友好。如果要改成1_0_1或1-0-1服务端是可以了,但APP端又得转下格式,怎么都有点别扭;
2)这个路由配得太死,1.0.1就是1.0.1的路由,APP端带1.0.2来就调不通。实际场景中后端没法完全对应APP版本,APP甚至连审核不通过重新提交都需要修改版本号,后端必须能向前兼容;
3)最后一个问题最致命:方法不可以复用,1.0.1版本的UserController里写了10个方法,1.0.2版只改了其中1个方法,还得把其他9个方法都原样搬过来。
理想的效果应该是这样:APP请求接口的时候把版本号(这个版本号就是上架各大商店的版本号)放到请求header里,服务端根据版本号找控制器,找到了直接调用,没有的话就向前找;同时方法也要向前找,带着这些需求寻找解决方案。
versioning
找下网络上的方案,aspnet-api-versioning用的最多,原来不止我一个人有这个需求。这个Versioning是微软出的,同时支持Core,简直靠谱,建个Web API项目试试
NuGet安装
Web API:Install-Package Microsoft.AspNet.WebApi.Versioning
Core:Install-Package Microsoft.AspNetCore.Mvc.Versioning
修改WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
); config.AddApiVersioning(c =>
{
c.ReportApiVersions = true;//是否在请求头中返回受支持的版本信息
c.ApiVersionReader = new HeaderApiVersionReader("version");//header里版本号的键名
c.AssumeDefaultVersionWhenUnspecified = true;//请求没有指明版本的情况下是否使用默认的版本
c.DefaultApiVersion = new ApiVersion(, );//默认的版本号
});
}
}
新建两个UserController
代码
namespace webApiVersioning.Controllers.v1
{
[ApiVersion("1.0")]
public class UserController : ApiController
{
[HttpGet]
public string Get()
{
return "1.0";
}
}
}
namespace webApiVersioning.Controllers.v2
{
[ApiVersion("2.0")]
public class UserController : ApiController
{
[HttpGet]
public string Get()
{
return "2.0";
}
}
}
header里version分别传1.0和2.0效果如下
从输出来看,前端调用相同的接口,通过传递不同的version参数触发了服务端不同控制器的方法。但是缺点明显:
1)版本号没法配置成三段式的,现在所有商店里APP的版本都是三段式的;
2)跟上面的路由方案一样,没法向前找控制器。verson传3.0,不会自动找到2.0的控制器,不够灵活;
3)更不支持向前找方法。。。
终极方案
微软出的Versioning既然不能满足需求就自己实现一个,难点就一个:根据APP端传来的版本号触发对应的控制器。
版本号好获取,控制器也可以建个User{version}Controller,那怎么做到APP端带101的版本号请求UserController,服务端根据传的101自动转到UserController呢?
利用搜索引擎找到了System.Web.Http.Dispatcher命名空间下的DefaultHttpControllerSelector类,下面有个GetControllerName方法
// 摘要:
// 获取指定 System.Net.Http.HttpRequestMessage 的控制器的名称。
//
// 参数:
// request:
// HTTP 请求消息。
//
// 返回结果:
// 指定 System.Net.Http.HttpRequestMessage 的控制器的名称。
public virtual string GetControllerName(HttpRequestMessage request)
根据请求request获取指定控制器名称,那岂不是可以请求UserController,实际触发User101Controller了。建一个CustomHeaderControllerSelector继承DefaultHttpControllerSelector类,重写下GetControllerName方法试一试
public class CustomHeaderControllerSelector : DefaultHttpControllerSelector
{
public CustomHeaderControllerSelector(HttpConfiguration cfg) : base(cfg)
{ } public override string GetControllerName(HttpRequestMessage request)
{
var controllerRequest = base.GetControllerName(request);//User
return controllerRequest + "";//User101
}
}
注册到Global.asax里
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new CustomHeaderControllerSelector(GlobalConfiguration.Configuration));
}
}
新建一个User101Controller
public class User101Controller : ApiController
{
[HttpGet]
public string UserInfo()
{
return "";
}
}
项目里并没有UserController,请求下试试
可以看出,请求的是UserController,触发的是User101Controller,完美了。再建一个User102Controller
public class User102Controller : ApiController
{
[HttpGet]
public string UserInfo()
{
return "";
}
}
完善下GetControllerName方法
public override string GetControllerName(HttpRequestMessage request)
{
var controllerRequest = base.GetControllerName(request);
if (request.Headers.Contains("version"))
{
var headerVersion = request.Headers.GetValues("version").First();
if (string.IsNullOrEmpty(headerVersion))
return controllerRequest;
else
return controllerRequest + headerVersion.Replace(".", "");
}
return controllerRequest;
}
分别请求下试试
至此,已经实现了和微软的Versioning一模一样的功能,并且还支持了三段式的版本号。
下一个问题:如何向前找控制器?例:APP版本号到了1.0.2,但是1.0.2这个版本服务端只改了一个控制器的,大部分控制器都没有1.0.2版,如何调用1.0.2实际触发1.0.1呢?
场景:APP 1.0.1版上线,服务端有两个控制器 User101Controller、Employee101Controller。1.0.2版User控制器有接口变化,加了一个User102Controller,Employee控制器无变化不加,1.0.2上线后服务端就有3个控制器了,APP端调所有接口传的都是当前版本号1.0.2,访问User触发的是UserController,访问Employee触发的是EmployeeController,这就是所谓的向前兼容。
如何实现呢?还是在GetControllerName方法里做文章
如图DefaultHttpControllerSelector里还有另一个虚方法GetControllerMapping,这个方法可以获取当前项目里所有的控制器名称。这就简单了:获取到的是User101、User102,如果请求来的是User102直接返回User102,如果请求来的是User103,那么挨个比大小,还是返回User102。此方法可行,但是思考了一下,每次请求过来都要做如下操作:
1)获取项目所有控制器;
2)截取控制器名称后的版本号;
3)排序;
4)比大小。
步骤多,有点浪费性能,而且还容易出错。直接使用配置的方式,只比大小,其他步骤都省略。新建PublicConst.cs,把项目所有的控制器和对应的版本号都写到VersionList这个全局变量里
public class PublicConst
{
public static List<Vsn> VersionList
{
get
{
return new List<Vsn>()
{
//注:版本必须【由小到大】添加
new Vsn() { Controller = "User", Ver = "1.0.1" },
new Vsn() { Controller = "User", Ver = "1.0.2" },
};
}
}
} public class Vsn
{
public string Controller { get; set; }
public string Ver { get; set; }
}
现在请求过来是1.0.3,从配置里拿到了1.0.1和1.0.2,常规做法是根据“.”分割,然后挨个比数字大小,还要考虑4段、5段以及段数不一样的版本号比大小,这样代码写着就没边了,还容易出bug。System命名空间下有Version类,调方法就可以比大小,用起来
public static string CompareVersion(string version, List<string> vsns)
{
var newestVersion = string.Empty;
if (string.IsNullOrEmpty(version) || vsns.Count == )
{
return newestVersion;
} Version verCurrent = new Version(version);
for (int i = vsns.Count - ; i >= ; i--)//倒叙查提高效率:新版的用户比老用户多
{
if (verCurrent >= new Version(vsns[i]))
{
newestVersion = vsns[i];
break;
}
}
return newestVersion;
}
按照上面的思路继续改造下GetControllerName方法
public override string GetControllerName(HttpRequestMessage request)
{
var controllerRequest = base.GetControllerName(request);
var controllerWithVersion = controllerRequest;
if (request.Headers.Contains("version"))
{
var headerVersion = request.Headers.GetValues("version").First();
if (!string.IsNullOrEmpty(headerVersion))
{
controllerWithVersion = controllerRequest + headerVersion.Replace(".", "");
if (!GetControllerMapping().ContainsKey(controllerWithVersion))
{
var lsVersion = PublicConst.VersionList.Where(x => x.Controller == controllerRequest).ToList().ConvertAll(t => t.Ver);
var newestVersion = Utils.CompareVersion(headerVersion, lsVersion);
if (!string.IsNullOrEmpty(newestVersion))
controllerWithVersion = controllerRequest + newestVersion.Replace(".", "");
else
controllerWithVersion = controllerRequest;
}
}
}
return controllerWithVersion;
}
调用试试
由图可见,已经能向前找控制器了。还剩一个问题:如何向前找方法?场景:APP已经更新到1.0.3了,但是有一个方法一直没有修改,还在1.0.1的控制器里面,1.0.2里面都没有这个方法。这个时候直接调1.0.3的控制器可以定位到1.0.2控制器没问题,但是1.0.2控制器下没有这个方法,如何转到1.0.1控制器,并调用其下面的这个方法呢?
回头看看刚才的接口配置对象VersionList
public static List<Vsn> VersionList
{
get
{
return new List<Vsn>()
{
//注:版本必须【由小到大】添加
new Vsn() { Controller = "User", Ver = "1.0.1" },
new Vsn() { Controller = "User", Ver = "1.0.2" },
};
}
}
在这个里面配置每个版本控制器里面包含的方法,每次对比版本的时候判断下方法是否存在即可
public static List<Vsn> VersionList
{
get
{
return new List<Vsn>()
{
//注:版本必须【由小到大】添加
new Vsn() { Controller = "User", Ver = "1.0.1", Actions = new List<string>(){ "Get","Test"} },
new Vsn() { Controller = "User", Ver = "1.0.2", Actions = new List<string>(){ "Get"} },
};
}
}
弊端:方法多了,维护太费劲,怎么能实现自动向前找方法呢?这里就要返璞归真了,想想面向对象三大特征:继承、封装和多态
使用继承的方式即可。第一个版本全是虚方法,后面每个版本继承上一个版本,如果接口有修改就override上一个版本的接口即可。
User101Controller代码:
public class User101Controller : ApiController
{
[HttpGet]
public virtual string UserInfo()
{
return "";
} [HttpGet]
public virtual string Test()
{
return "Test:101";
}
}
User102Controller代码:
public class User102Controller : User101Controller
{
[HttpGet]
public override string UserInfo()
{
return "";
}
}
请求下试试
根据VersionList维护的版本号,没有User103就找User102,但是User102下面并没有Test方法,Test方法在User101下面,这个时候由于User102继承了User101,也可以找到User101下的Test方法。
APP 1.0.3版本User控制器没有变化就不建控制器,1.0.4的时候User控制器加了一个Test2方法继续virtual,等待被下一个版本override
public class User104Controller : User102Controller
{
[HttpGet]
public virtual string Test2()
{
return "Test2:104";
}
}
记得维护到VersionList里
public static List<Vsn> VersionList
{
get
{
return new List<Vsn>()
{
//注:版本必须【由小到大】添加
new Vsn() { Controller = "User", Ver = "1.0.1" },
new Vsn() { Controller = "User", Ver = "1.0.2" },
new Vsn() { Controller = "User", Ver = "1.0.4" },
};
}
}
看下最终成果
至此,大功告成。
以上是我的方案,仅抛砖引玉,欢迎大家补充。 本文地址:https://www.cnblogs.com/oppoic/p/13367878.html
思考
看下核心的GetControllerName方法
public override string GetControllerName(HttpRequestMessage request)
{
var controllerRequest = base.GetControllerName(request);
var controllerWithVersion = controllerRequest;
if (request.Headers.Contains("version"))
{
var headerVersion = request.Headers.GetValues("version").First();
if (!string.IsNullOrEmpty(headerVersion))
{
controllerWithVersion = controllerRequest + headerVersion.Replace(".", "");
if (!GetControllerMapping().ContainsKey(controllerWithVersion))
{
var lsVersion = PublicConst.VersionList.Where(x => x.Controller == controllerRequest).ToList().ConvertAll(t => t.Ver);
var newestVersion = Utils.CompareVersion(headerVersion, lsVersion);
if (!string.IsNullOrEmpty(newestVersion))
controllerWithVersion = controllerRequest + newestVersion.Replace(".", "");
else
controllerWithVersion = controllerRequest;
}
}
}
return controllerWithVersion;
}
本方案向前找控制器是通过对比版本大小实现的,如果接口访问频率超高(比如APP日活百万),有个优化方案:看看上面代码11行,如果当前请求的控制器不存在就向前找,把线上APP最新版本的控制器都建一遍,直接继承上一个控制器,不需要重写任何方法,这样就不用对比版本了,提升了性能。
另外再看版本号维护类VersionList
public static List<Vsn> VersionList
{
get
{
return new List<Vsn>()
{
//注:版本必须【由小到大】添加
new Vsn() { Controller = "User", Ver = "1.0.1" },
new Vsn() { Controller = "User", Ver = "1.0.2" },
new Vsn() { Controller = "User", Ver = "1.0.4" },
};
}
}
版本号对比是挨个对比,没法集中对比,所以必须按顺序维护起来,但是如果哪天粗心搞错了,或者添加了重复的版本号怎么处理?在Application_Start项目启动的时候检测下这个VersionList即可。
最后说下版本号,UserCotroller到底表示哪个版本?11.1.0还是1.11.0,这块也可以优化下,建成User1_11_0Controller就可以了。
环境
Visual Studio 2017 .NET Framework 4.5.2 Web API
参考
https://www.cnblogs.com/linhuiy/p/12668535.html
https://www.dotnetcurry.com/aspnet/1185/aspnet-web-api-versioning-angularjs-app
大家是怎么做APP接口的版本控制的?欢迎进来看看我的方案。升级版的Versioning的更多相关文章
- PHP做APP接口时,如何保证接口的安全性??????????
PHP做APP接口时,如何保证接口的安全性? 1.当用户登录APP时,使用https协议调用后台相关接口,服务器端根据用户名和密码时生成一个access_key,并将access_key保存在sess ...
- php做APP接口开发,接口的安全性
1.当用户登录APP时,使用https协议调用后台相关接口,服务器端根据用户名和密码时生成一个access_key,并将access_key保存在session(或者保存在redis)中,将生成的ac ...
- WebApi Swagger 接口多版本控制 适用于APP接口管理
最近研究了下swagger多版本的维护,网上的文章千篇一律,无法满足我的需求,分享下我的使用场景以及实现 演示环境:Visual Studio 2019.Asp.NET WebAPI.NET Fram ...
- 关于APP接口设计(转)
最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...
- 关于APP接口设计
最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...
- 关于APP接口设计 (转)
转自:http://blog.csdn.net/gebitan505/article/details/37924711 1.效率:接口访问速度 PHP建议使用YAF框架. 最好使用JSON格式数据,因 ...
- app接口开发
最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...
- PHP开发APP接口实现--基本篇
最近一段时间一直在做APP接口,总结一下APP接口开发以来的心得,与大家分享: 1. 客户端/服务器接口请求流程: 安卓/IOS客户端 –> PHP接口 –> 服务器端 –> ...
- 《PHP开发APP接口》笔记
PHP开发APP接口 [TOC] 课程地址 imooc PHP开发APP接口 学习要点 APP接口简介 封装通信接口方法 核心技术 APP接口实例 服务器端 -> 数据库|缓存 -> 调用 ...
随机推荐
- 【Flutter实战】自定义滚动条
老孟导读:[Flutter实战]系列文章地址:http://laomengit.com/guide/introduction/mobile_system.html 默认情况下,Flutter 的滚动组 ...
- 部署rabbitMQ镜像集群实战测试
部署rabbitMQ镜像集群 版本信息 rabbit MQ: 3.8.5 Erlang: 官方建议最低21.3 推荐22.x 这里用的是23 环境准备 主机规划 主机 节点 172.16.14.3 磁 ...
- Oracle 11g各种服务作用以及哪些需要开启
Windwos server 2012 R2上成功安装Oracle 11g后共有7个服务,如果全局数据库名为orcl,则Oracle服务分别为 Oracle ORCL VSSWriter Servic ...
- 飞越面试官(三)--JVM
大家好!我是本公众号唯一官方指定没头屑的小便--怕屁林. JVM,全称Java Virtual Machine,作为执行Java程序的容器,几乎代理了Java内存与服务器内存的交互,可以说是程序拥 ...
- Mybatis架构相关的知识
如上所示,这是一个简单的Mybaits执行流程. 我们其实可以看到,一直到第三步(Sqlsession)那么一步,这都是我们的程序里需要创建的.而之后的步骤才是底层完成的任务. 这里就有了一个引申的概 ...
- 每日一题 - 剑指 Offer 38. 字符串的排列
题目信息 时间: 2019-06-29 题目链接:Leetcode tag:深度优先搜索 回溯法 难易程度:中等 题目描述: 输入一个字符串,打印出该字符串中字符的所有排列. 你可以以任意顺序返回这个 ...
- CSS3样式_实现字体发光效果
text-shadow 属性仅仅是用来设置文本阴影的,似乎并不能实现字体发光效果.其实不然,这正是 text-shadow 属性的精妙之处.当阴影的水平偏移量和垂直偏移量都为0时,阴影就和文本重合了. ...
- URL编码与二次encodeURI
转自:http://foryougeljh.iteye.com/blog/1456706 一般来说,URL只能使用英文字母.阿拉伯数字和某些标点符号,不能使用其他文字和符号.比如,世界上有英文字母的网 ...
- 干货分享丨jvm系列:dump文件深度分析
摘要:java内存dump是jvm运行时内存的一份快照,利用它可以分析是否存在内存浪费,可以检查内存管理是否合理,当发生OOM的时候,可以找出问题的原因.那么dump文件的内容是什么样的呢? JVM ...
- day67 前后端数据交互
目录 一.前后端传输数据的编码格式(contentType) 1 form表单 2 ajax请求 二.ajax发送json格式数据 三.ajax发送文件 四.django自带的序列化组件(drf做铺垫 ...