一. 背景和方案

1. 多版本管理的概念

  Android 、IOS等 App 存在着多版本客户端共存的问题:App 最新版已经升级到了5.0 了,但是有的用户手机上还运行着 4.8、3.9 甚至2.2 版本的 App,由于早期没有内置升级机制、用户不会升级、用户拒绝升级等原因,造成这些旧版本 App 也在运行。开发新版本 App 的时候,要给接口增加新的功能或者修改以前接口的规范,会造成旧版本App 无法使用,因此在一定情况下会“保留旧接口的运行、新功能用新接口”,这样就会存在多版本接口共存的问题。

  通常的做法是:旧版接口做一个代码分支,除了进行 bug 修改外,旧版本接口不再做改动,新接口代码继续演化升级。在客户端请求的时候带着要请求的接口版本号,在服务器端选择合适的版本代码进行处理。

2. 解决方案

(1). 不同的版本使用不同的域名:v1.api.ypf.com、v2.api.ypf.com、v3……  (最佳方案)

(2). 在Url,报文头等中带不同的版本信息,用Nginx等做反向代理服务,然后将 http://api.ypf.com/api/v1/User/1http://api.ypf.com/api/v2/User/1 转到不同的服务器处理。

(3). 多个版本的 Controller共处在一个项目中,然 后使 用 [RoutePrefix] 特性来进行区分,这种方案Controller的名字不能一样,如下:

(4). 如果我想在Controller文件夹中新建多个版本文件夹,如:v1、v2、v3,每个文件夹中存放的控制器名称相同,比如都叫PersonController,不同文件夹下代表不同版本,这个时候会有一个很尴尬的问题,没法请求,识别不了,这个时候就需要重写系统默认的机制,IHttpControllerSelector 根据 “报文头”或者“请求路径”等选择不同的 Controller 执行。

该方案的实现,详见下面的实战测试

二. 实战测试

1. 在Controller文件下新建v1和v2文件夹,分别存放不同版本的Person控制器,每个Person控制器新建一个GetName方法,如下图:

2. 重写系统默认的控制器选择机制,可以直接实现IHttpControllerSelector接口,也可以继承DefaultHttpControllerSelector类,从而间接实现IHttpControllerSelector接口,在重写的SelectController方法中,实现了两种区分机制,分别是根据请求路径区分 和 根据报文头中的ApiVersion参数区分。

     /// <summary>
/// 自己实现IHttpControllerSelector接口来替换系统默认的IHttpControllerSelector
/// </summary>
public class VersionConstrollerSelector : DefaultHttpControllerSelector
{ private HttpConfiguration _config;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="config"></param>
public VersionConstrollerSelector(HttpConfiguration config) : base(config)
{
_config = config;
} /// <summary>
/// 获取所有的Controller
/// </summary>
/// <returns></returns>
public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
{
Dictionary<string, HttpControllerDescriptor> dict = new Dictionary<string, HttpControllerDescriptor>();
//循环所有的程序集
foreach (var asm in _config.Services.GetAssembliesResolver().GetAssemblies())
{
//获取所有继承ApiController的非抽象类
var controllerTypes = asm.GetTypes().Where(t => !t.IsAbstract && typeof(ApiController).IsAssignableFrom(t)).ToArray();
//循环上述获取的非抽象类
foreach (var ctrlType in controllerTypes)
{
//从namespace中提取版本号
var match = Regex.Match(ctrlType.Namespace, @"_05_WebApiExtend.Controllers.v(\d+)");
if (match.Success)
{
//获取版本号
string verNum = match.Groups[].Value;
//获取控制器名称(eg:PersonController中获取Person)
string controllerName = Regex.Match(ctrlType.Name, "(.+)Controller").Groups[].Value;
//声明集合中的键(形式:Personv1 、Personv2)
string key = controllerName + "v" + verNum;
dict[key] = new HttpControllerDescriptor(_config, controllerName, ctrlType);
} } }
return dict;
} /// <summary>
/// 进行Controller的匹配
/// </summary>
/// <param name="request"></param>
/// <returns>匹配成功返回控制器信息,匹配失败返回null</returns> public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
//获取所有的controller键值集合
var controllers = GetControllerMapping();
//获取路由数据
var routeData = request.GetRouteData();
//从路由中获取当前controller的名称
var controllerName = (string)routeData.Values["controller"]; //下面是两种方式获取版本号
string verNum = "";
try
{
//从报文头中获取版本号(当没有这个参数的时候走catch)
verNum = request.Headers.GetValues("ApiVersion").Single();
}
catch (Exception)
{
//从url中获取版本号
verNum = Regex.Match(request.RequestUri.PathAndQuery, @"api/v(\d+)").Groups[].Value;
}
//拼接key值
string key = controllerName + "v" + verNum;
if (controllers.ContainsKey(key))
{
return controllers[key];
}
else
{
return null;
}
}
}

3.  在WebConfig.cs类中,加上 config.Services.Replace(typeof(IHttpControllerSelector), new VersionConstrollerSelector(config)); ,即用重写IHttpControllerSelector的替换原先的IHttpControllerSelector,如下图:

4.  注释掉原先的路由请求模式,新增下面两个路由规则

  public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 配置和服务
//用重写IHttpControllerSelector的替换原先的IHttpControllerSelector
config.Services.Replace(typeof(IHttpControllerSelector), new VersionConstrollerSelector(config));
// Web API 路由
config.MapHttpAttributeRoutes();
//config.Routes.MapHttpRoute(
// name: "DefaultApi",
// routeTemplate: "api/{controller}/{action}/{id}",
// defaults: new { id = RouteParameter.Optional }
//); //多版本控制的路由改造
config.Routes.MapHttpRoute(
name: "DefaultApiV1",
routeTemplate: "api/v1/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
); config.Routes.MapHttpRoute(
name: "DefaultApiV2",
routeTemplate: "api/v2/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}

5. 用PostMan测试

(1). 分别请求 http://localhost:2182/api/v1/Person/GetName?Name=2   和 http://localhost:2182/api/v2/Person/GetName?Name=2, 返回不同的版本的信息,证明可以根据Url中的v1和v2进行版本区分。

(2). 请求 http://localhost:2182/api/v1/Person/GetName?Name=2 地址两次 ,表头中分别带有ApiVersion=1 和 ApiVersion=2,返回不同版本的信息,证明可以报文头中的参数进行版本区分。

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

第十一节:WebApi的版本管理的几种方式的更多相关文章

  1. 第三百九十一节,Django+Xadmin打造上线标准的在线教育平台—404,403,500页面配置

    第三百九十一节,Django+Xadmin打造上线标准的在线教育平台—404,403,500页面配置 路由映射在全局也就是根目录里的urls.py里配置404路由映射 注意:不是写在urlpatter ...

  2. 第三百八十一节,Django+Xadmin打造上线标准的在线教育平台—xadmin全局配置

    第三百八十一节,Django+Xadmin打造上线标准的在线教育平台—xadmin全局配置 1.xadmin主题设置 要使用xadmin主题,需要在一个app下的adminx.py后台注册文件里,写一 ...

  3. 第三百七十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现我的搜索以及热门搜索

    第三百七十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现我的搜索以及热门 我的搜素简单实现原理我们可以用js来实现,首先用js获取到 ...

  4. 第三百六十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)倒排索引

    第三百六十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)倒排索引 倒排索引 倒排索引源于实际应用中需要根据属性的值来查找记录.这种索引表中的每一项都包 ...

  5. 第三百五十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—将selenium操作谷歌浏览器集成到scrapy中

    第三百五十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—将selenium操作谷歌浏览器集成到scrapy中 1.爬虫文件 dispatcher.connect()信号分发器,第一个参数信 ...

  6. 第三百四十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—编写spiders爬虫文件循环抓取内容—meta属性返回指定值给回调函数—Scrapy内置图片下载器

    第三百四十一节,Python分布式爬虫打造搜索引擎Scrapy精讲—编写spiders爬虫文件循环抓取内容—meta属性返回指定值给回调函数—Scrapy内置图片下载器 编写spiders爬虫文件循环 ...

  7. 第三百三十一节,web爬虫讲解2—Scrapy框架爬虫—Scrapy安装—Scrapy指令

    第三百三十一节,web爬虫讲解2—Scrapy框架爬虫—Scrapy安装—Scrapy指令 Scrapy框架安装 1.首先,终端执行命令升级pip: python -m pip install --u ...

  8. 第三百二十一节,Django框架,发送邮件

    第三百二十一节,Django框架,发送邮件 全局配置settings.py EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' ...

  9. 第三百一十一节,Django框架,Form表单验证

    第三百一十一节,Django框架,Form表单验证 表单提交 html <!DOCTYPE html> <html lang="en"> <head& ...

随机推荐

  1. Linux-Redmine安装方法

    Linux-Redmine安装方法 QQ群交流:585499566 一.环境准备 1,Linux系统:centos6.5 2,Redmine安装包:bitnami-redmine-3.4.6-0-li ...

  2. Structs2 中拦截器获取请求参数

    前言 环境:window 10,JDK 1.7,Tomcat 7 测试代码 package com.szxy.interceptor; import java.util.Map; import jav ...

  3. EF6实现软删除

    https://www.jianshu.com/p/c65fbfe16e1a

  4. NT路径,DOS路径和Device路径互相转换

    项目中遇到的比较奇葩的问题,从网上找到一份源码,https://blog.csdn.net/qq125096885/article/details/70766206 稍微整理了下,VS可以直接编译 # ...

  5. .net问号的作用

    ??运算符(C# 参考)http://msdn.microsoft.com/zh-cn/library/ms173224.aspx 可以为 null 的类型(C# 编程指南)http://msdn.m ...

  6. 智表ZCELL产品V1.4.0开发API接口文档 与 产品功能清单

    为了方便大家使用ZCELL,应网友要求,整理编写了相关文档,现与产品一起同步发布,供大家下载使用,使用过程中如有疑问,请与我QQ联系. 智表(ZCELL)V1.4.0版本  功能清单文档下载地址: 功 ...

  7. 使用Crawler框架搭建自己的爬虫框架MyCrawler

    自己写一个爬虫框架的目的: 完美架构 在实际的数据采集编码过程中,发现代码比较乱,抓取数据,存储数据的代码混杂在一起,为了构建比较完美的数据采集框架 敏捷开发 将数据采集进行标准流程化,每个标准流程都 ...

  8. JS 基础知识点

    最近发现一个好东西,掘金小册,觉得里面的东西挺不错的,准备仔细阅读一下,提升下自己. 记录一下,随便加深点儿印象,主要内容源自于小册. 原始类型 原始类型也成为基本数据类型 boolean null ...

  9. Codeforces Round #522 (Div. 2, based on Technocup 2019 Elimination Round 3)B. Personalized Cup

    题意:把一长串字符串 排成矩形形式  使得行最小  同时每行不能相差大于等于两个字符 每行也不能大于20个字符 思路: 因为使得行最小 直接行从小到大枚举即可   每行不能相差大于等于两个字符相当于  ...

  10. M-BM-

    今天拷贝了一段代码 struct    Test       {               Test(    int    )    {}               Test()    {}    ...