最近研究了下swagger多版本的维护,网上的文章千篇一律,无法满足我的需求,分享下我的使用场景以及实现

演示环境:Visual Studio 2019、Asp.NET WebAPI、NET Framework 4.5.2、Swashbuckle.Core 5.6.0

本文地址:https://www.cnblogs.com/oppoic/p/14380233.html

一、背景

BS应用没有接口版本的概念,因为网站一上线,接口和页面都是新的,服务端不需要维护老接口

但是对于手机APP,服务端就必须要考虑老版本的接口了,因为用户如果不更新APP,老版本的接口必须存在,这就有了接口版本的概念

二、我们的使用场景

我司APP开发调服务端接口的时候,喜欢把版本号放到请求Header里面,这个版本号就是APP上架各大商店的版本号,大概是这样的

如图,1.9.9版本APP调用服务端接口,Header里的Version就是1.9.9。迭代到2.0.0,调同样的接口带的版本号就变成了2.0.0,服务端怎么处理呢?

常规做法是通过路由实现,但实际情况是这样的:上架苹果App Store顺利通过,版本号为1.9.9。但是上架华为应用市场,因为软著的问题被拒了,再次提交版本号就变成了2.0.0。其实APP内部没有任何改变,服务端这个时候再加上2.0.0的所有接口,然后再次发版吗?理想的状态应该是这样:

  • 版本号可以向前兼容,服务端没有2.0.0版本的接口就自动找1.9.9版本的接口;
  • 接口可以复用,例:2.0.0版本只修改了1.9.9版本的1个接口,其他接口的实现都是一样的,那就没必要把1.9.9版本的接口都拷贝到2.0.0;
  • 计算版本号一定要快,因为随着APP的迭代,服务端维护的版本可能特别多,计算慢的话接口访问速度会越来越差

以上需求都实现好了,具体请参考:大家是怎么做APP接口的版本控制的?欢迎进来看看我的方案。升级版的Versioning

接下来才是本篇文章的重点,服务端接口都写好了,怎么提供给前端同事查看呢?

三、和swagger结合

每次写完接口都录进文档太麻烦了,以后修改还要维护,如果能自动生成文档就好了。swagger就是解决这个问题的

新建一个空的 Asp.net WebAPI 程序(非Core程序)并安装下swagger

Asp.net WebAPI 安装的是 Swashbuckle.Core,只要安装一个即可,swagger页面、js、css等文件都打包在这个dll里面。结合前篇文章已经实现的服务端接口多版本控制,现在项目结构如下

看下几个控制器的代码

using System.Web.Http;

namespace WebAPISwaggerVersioning.Controllers.v1
{
public class Employee_1_0_0_Controller : ApiController
{
[HttpGet]
public virtual string Get()
{
return "1.0.0";
} [HttpGet]
public virtual string GetEmployee()
{
return "GetEmployee:1.0.0";
}
}
}

1.0.0 版本的 Employee 控制器有两个虚方法:GetGetEmployee,因为是虚方法,如果下一个版本同样的接口有变化的话,直接 override 即可

接下来,看看 1.0.1 版本的 Employee 控制器

using System.Web.Http;

namespace WebAPISwaggerVersioning.Controllers.v1
{
public class Employee_1_0_1_Controller : Employee_1_0_0_Controller
{
[HttpGet]
public override string Get()
{
return "1.0.1";
} [HttpGet]
public virtual string GetEmployeeList()
{
return "GetEmployeeList:1.0.1";
}
}
}

1.0.1 版本的 Employee 控制器重写了 1.0.0 版本的 Get 方法,并加了一个新的虚方法 GetEmployeeList,因为继承了上一个版本,所以还有一个继承过来的方法 GetEmployee

再看看 2.0.0 版本

using System.Web.Http;
using WebAPISwaggerVersioning.Controllers.v1; namespace WebAPISwaggerVersioning.Controllers.v2
{
public class Employee_2_0_0_Controller : Employee_1_0_1_Controller
{
[HttpGet]
public override string Get()
{
return "2.0.0";
} [HttpGet]
public override string GetEmployee()
{
return "GetEmployee:2.0.0";
}
}
}

2.0.0 版本接着继承上一个版本,同时重写了 GetGetEmployee 方法

swagger的配置类 SwaggerConfig.cs

using System.Web.Http;
using Swashbuckle.Application; namespace WebAPISwaggerVersioning
{
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly; GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "项目名称");
})
.EnableSwaggerUi(c =>
{
c.DocumentTitle("WebAPISwaggerVersioning");
});
}
}
}

直接运行起来看看效果

真的不错,安装了swagger并简单配置就有了这样的效果,但是有几个问题

  • 没有区分版本:1.x 和 2.x 的接口都在一个页面;
  • 直接把 控制器名称版本号 都读取出来了:/api/Employee_1_0_0_/Get,前端调用其实是这样的:/api/Employee/Get,版本号携带在请求Header里;
  • 另外把 继承的方法 也读取出来了:Employee_1_0_1_Controller 下并没有 GetEmployee 方法,继承的方法不需要展示,否则太多了

现在开始改进

public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
var xmlPath = string.Format("{0}/bin/WebAPISwaggerVersioning.xml", AppDomain.CurrentDomain.BaseDirectory); GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.MultipleApiVersions((apiDesc, targetApiVersion) => ResolveVersionSupportByRouteConstraint(apiDesc, targetApiVersion), (v) =>
{
v.Version("v1", "版本1.x").Description("1.x接口文档。点击右上角下拉列表,查看新版本接口");
v.Version("v2", "版本2.x").Description("增加了手机号找回密码、财务报销等功能");
});
})
.EnableSwaggerUi(c =>
{
c.DocumentTitle("WebAPISwaggerVersioning");
c.EnableDiscoveryUrlSelector();//下拉列表列出版本信息
});
} /// <summary>
/// 返回特定版本下的接口
/// </summary>
/// <param name="apiDesc"></param>
/// <param name="targetApiVersion"></param>
/// <returns></returns>
private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
{
var controllerFullName = apiDesc.ActionDescriptor.ControllerDescriptor.ControllerType.FullName;
return controllerFullName.Split('.').Contains(targetApiVersion, StringComparer.OrdinalIgnoreCase);
}

通过 MultipleApiVersions 方法开启了多版本

注:配置的 v1 和 v2 必须和文件夹名称相同,因为 ResolveVersionSupportByRouteConstraint 方法是通过命名空间来区分版本的,运行看下效果

2.x 的控制器已经不在这个页面显示了,但是丑陋的 Employee_1_0_0_ 对前端不友好

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class SwaggerControllerViewAttribute : Attribute
{
/// <summary>
/// 控制器名称
/// </summary>
public string ControllerName { get; private set; } /// <summary>
/// 版本号
/// </summary>
public string Version { get; private set; } /// <summary>
/// Swagger文档显示
/// </summary>
/// <param name="cName">控制器名称</param>
/// <param name="version">版本号</param>
public SwaggerControllerViewAttribute(string cName, string version)
{
ControllerName = string.IsNullOrEmpty(cName) ? "请填写控制器名称" : cName;
Version = string.IsNullOrEmpty(version) ? "请填写版本号" : version;
}
}

建一个特性 SwaggerControllerViewAttribute ,标注到控制器上

[SwaggerControllerView("员工", "v1.0.0")]
public class Employee_1_0_0_Controller : ApiController

再利用 GroupActionsBy 方法读取特性为控制器分组

c.GroupActionsBy(apiDesc =>
{
System.Diagnostics.Debug.WriteLine(apiDesc.ID);
var attribute = apiDesc.GetControllerAndActionAttributes<SwaggerControllerViewAttribute>();
if (attribute.Any())
return attribute.First().ControllerName + " " + attribute.First().Version;
else
return apiDesc.ActionDescriptor.ControllerDescriptor.ControllerName;
});

看下效果

标注在控制器上的名称已经读取出来了,再把接口后面的版本号干掉

/// <summary>
/// 自定义文档过滤器
/// </summary>
internal class CustomDocumentFilter : IDocumentFilter
{
/// <summary>
/// Apply
/// </summary>
/// <param name="swaggerDoc">文档</param>
/// <param name="schemaRegistry">schema注册</param>
/// <param name="apiExplorer">api概览</param>
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
{
//多版本接口名修正
var match = new Dictionary<string, PathItem>();
foreach (var path in swaggerDoc.paths)
{
var lsXG = path.Key.Split('/');
if (lsXG.Count() == 4)
{
var lsXXG = lsXG[2].Split('_');
if (lsXXG.Count() == 5)
{
match.Add("/" + lsXG[1] + "/" + lsXXG[0] + "/" + lsXG[3] + "?version=v" + lsXXG[1] + "." + lsXXG[2] + "." + lsXXG[3], path.Value);
}
}
}
swaggerDoc.paths = match;
}
}

swaggerDoc.paths 就是所有接口,继承 IDocumentFilter 接口实现 Apply 方法,可以自定义接口名称,想怎么显示就怎么显示

接口名称已经修正了,但是有个遗憾,因为 swaggerDoc.paths 是字典类型的,key不能重复,所以每个接口后面都跟着 version=,稍后通过前端注入js把 ?version=xxx 去掉

四、柳暗花明

本以为大功告成了,但是注意看 /api/Employee/GetEmployee?version=v1.0.1 这个接口不应该出现,如果把每个继承过来的方法都显示出来了,那简直太乱了,前端只关注本次版本新增(virtual)和变更(override)的方法

到这块可把我难住了,试了很久,swagger没有提供任何一个接口可以解决这个问题。距离完美就差一点了,还是不死心,最后通过判断方法的父类解决了:父类是当前控制器就是新方法或者重写的方法,不是肯定就是继承过来的,直接移除不展示

foreach (var apiDesc in apiExplorer.ApiDescriptions)
{
var key = "/" + apiDesc.RelativePath;
if (!swaggerDoc.paths.ContainsKey(key)) continue;//swaggerDoc.paths是当前选择版本的接口,例:v1 var controllerName = apiDesc.ActionDescriptor.ControllerDescriptor.ControllerType.Name;
var actionName = apiDesc.ActionDescriptor.ActionName;
if (!string.IsNullOrEmpty(controllerName) && !string.IsNullOrEmpty(actionName))
{
var t = Type.GetType(apiDesc.ActionDescriptor.ControllerDescriptor.ControllerType.Namespace + "." + controllerName);
if (t != null)
{
var baseControllerName = t.GetMethod(actionName).DeclaringType.Name;
if (controllerName != baseControllerName)
{
if (key.Contains("?"))
key = key.Substring(0, key.IndexOf("?", StringComparison.Ordinal));
swaggerDoc.paths.Remove(key);//移除继承的Action,避免文档中重复展示
}
}
}
}

再向前端注入js解决接口后面带 ?version=xxx 的问题。是的,swagger就是这么灵活,后端前端都可以各种自定义

c.InjectJavaScript(thisAssembly, "WebApiSwaggerVersioning.Scripts.swagger.js");
$("#resources_container .resource").each(function (idx, item) {
$.each($(item).find(".endpoints .endpoint"), function (i, v) {
var path = $(v).find(".path a");
var pathTxt = path.text();
if (pathTxt) {
path.text(pathTxt.substring(0, pathTxt.indexOf('?')));
}
});
});

看看简洁的接口名称

接口已经完美了,同时注入的 swagger.js 里面还有汉化包,现在可以显示中文了。注:swagger.js 需要设置 右键 - 属性 - 生成操作 - 嵌入的资源

文档里 /api/Employee/Get 出现了两次,怎么区分调哪个版本呢?通过继承 IOperationFilter 实现向请求Header里加自定义参数

public class AuthHeaderFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.parameters == null) operation.parameters = new List<Parameter>(); var arr = new string[] { };
if (!string.IsNullOrEmpty(operation.operationId)) arr = operation.operationId.Split('_');
operation.parameters.Add(new Parameter { name = "version", @in = "header", description = "接口版本号", type = "string", @default = arr.Length > 4 ? arr[1] +
"." + arr[2] + "." + arr[3] : "" }); var filterPipeline = apiDescription.ActionDescriptor.GetFilterPipeline();//是否添加权限过滤器
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Instance).Any(filter => filter is IAuthorizationFilter);//是否允许匿名方法
var allowAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
if (isAuthorized && !allowAnonymous)
{
operation.parameters.Add(new Parameter { name = "token", @in = "header", description = "接口token", required = true, type = "string" });
}
}
}

为每个接口的Header里设置了两个参数:versiontoken,模拟APP端调接口传递的 版本号鉴权token

终极效果如下

调下 1.0.1 版本的 Get 接口

测试一个不存在的Version

前端即便传来了一个服务端没有的Version 1.0.5,也能自动向前找最近一个版本1.0.1的接口

至此,大功告成,最后看看对比图

五、结语

参考文章

源码

点我下载

WebApi Swagger 接口多版本控制 适用于APP接口管理的更多相关文章

  1. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [3] 首页 APP 接口开发方案 ② 读取缓存方式

    以静态缓存为例. 修改 file.php line:11 去掉 path 参数(方便),加上缓存时间参数: public function cacheData($k,$v = '',$cacheTim ...

  2. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [4] 首页 APP 接口开发方案 ③ 定时读取缓存方式

    用于 linux 执行 crontab 命令生成缓存的文件 crop.php <?php //让crontab 定时执行的脚本程序 require_once 'db.php'; require_ ...

  3. PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [2] 首页 APP 接口开发方案 ① 读取数据库方式

    方案一:读取数据库方式 从数据库读取信息→封装→生成接口数据 应用场景: 数据时效性比较高的系统 方案二:读取缓存方式 从数据库获取信息(第一次设置缓存或缓存失效时)→封装(第一次设置缓存或缓存失效时 ...

  4. 关于APP接口设计(转)

    最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...

  5. 关于APP接口设计

    最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...

  6. app接口开发

    最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...

  7. 《PHP开发APP接口》笔记

    PHP开发APP接口 [TOC] 课程地址 imooc PHP开发APP接口 学习要点 APP接口简介 封装通信接口方法 核心技术 APP接口实例 服务器端 -> 数据库|缓存 -> 调用 ...

  8. PHP开发APP接口

    第1章 APP接口简介 - 课程简介 (:) - APP接口介绍 (:) - 客户端APP通信 (:) 最近学习 - 客户端APP通信格式区别 (:) - APP接口做的哪些事儿 (:) 第2章 封装 ...

  9. PHP开发APP接口实现--基本篇

    最近一段时间一直在做APP接口,总结一下APP接口开发以来的心得,与大家分享: 1. 客户端/服务器接口请求流程: 安卓/IOS客户端   –> PHP接口 –> 服务器端  –> ...

随机推荐

  1. 【Java基础】Java11 新特性

    Java11 新特性 新增字符串处理方法 新增方法: 判断字符串是否为空白 " ".isBlank(); // true 去除首尾空白 " Javastack " ...

  2. MySQL学习Day01

    1.MySQL的层级关系 2.xampp的安装使用 如果之前安装过mysql那么就需要将原来的mysql完全卸载干净 1.卸载之前安装的MySQL 安装xampp需要先卸载之前的mysql,以及更改m ...

  3. spring cloud config —— git配置管理

    目录 talk is cheep, show your the code Server端 pom.xml server的application.yml 配置文件 测试Server client端 po ...

  4. 【Linux】centos 7中,开机不执行rc.lcoal中的命令

    最近将一些需要开机启动的命令添加到了rc.local中 本想着开机就启动了,很省事 但是一次意外的重启,发现rc.local中的全部命令都没有执行 发现问题后,及时查找 参考:https://blog ...

  5. 【ORACLE】11g rac+dg

    首先感谢群友分享的文档,在这里先感谢哆啦B梦,非常感谢 该文档主要指导如何利用现有的RAC环境搭建一套RAC与单实例的DG的环境  ============================主机配置信息 ...

  6. 汇编学习笔记——DOS及DEBUG介绍

    转自:https://www.shiyanlou.com/courses/running/332 一.课程简介 声明:该课程基于<汇编语言(第2版)>郑晓薇 编著,机械工业出版社.本节实验 ...

  7. Empire

    Empire 内网渗透神器 一 基本渗透 安装 git clone https://github.com/BC-SECURITY/Empire/ ./setup/install.sh 启动 ./emp ...

  8. undefined和null区别

    undefined类型只有一个值就是undefined,没有必要显式地声明一个变量为undefined. null类型其实就是一个对象的空指针,所以用typeof null 才会显示为object. ...

  9. 30分钟带你理解 Raft 算法

    为什么需要 Raft? Raft 是什么? Raft 的目标 前置条件:复制状态机 Raft 基础 Leader 选举(选举安全特性) 日志复制(Leader只附加.日志匹配) 安全 学习资料 使用 ...

  10. Docker容器日志清理方案

    Docker容器在运行过程中会产生很多日志,久而久之,磁盘空间就被占满了,以下分享docker容器日志清理的几种方法 删除日志 在linux上,容器日志一般存放在 /var/lib/docker/co ...