什么是API 的多版本问题?

Android 等App 存在着多版本客户端共存的问题:App 最新版已经升级到了5.0了,但是有的用户手机上还运行着4.8、3.9甚至2.2版本的App,由于早期没有内置升级机制、用户不会升级、用户拒绝升级等原因,造成这些旧版本App也在运行。

开发新版本App的时候,要给接口增加新的功能或者修改以前接口的规范,会造成旧版本App无法使用,因此在一定情况下会“保留旧接口的运行、新功能用新接口”,这样就会存在多版本接口共存的问题。

通常的做法是:旧版接口做一个代码分支,除了进行bug修改外,旧版本接口不再做改动;新接口代码继续演化升级。在客户端请求的时候带着要请求的接口版本号,在服务器端选择合适的版本代码进行处理。
技术处理方法:
1> (最推荐)不同版本用不同的域名:v1.api.rupeng.com、v2.api.rupeng.com、v3……;
2> 在url、报文头等中带不同的版本信息,用Nginx等做反向代理服务器,然后将http://api.rupeng.com/api/v1/User/1和http://api.rupeng.com/api/v2/User/1转到不同的服务器处理。

3> 多个版本的Controller共处在一个项目中,然后使用[RoutePrefix]或者IHttpControllerSelector

现在我们来看看多个版本的Controller共处在一个项目中这种情况下我们的如何处理

处理方法1:

通过个控制器和方法打RoutePrefix标签和Route标签的方式,根据路由规则,来选择哪个控制器

例如:

http://localhost:14483/api/v1/Person/1 这个URL地址的请求由PersonController控制来处理

http://localhost:14483/api/v2/Person/1 这个URL地址的请求由PersonV2Controller控制来处理


namespace WebApi.Controllers
{
    [RoutePrefix("api/v1/Person")]
    public class PersonController : ApiController
    {
        [Route("{id}")]
        public string Get(int id)
        {
            return "我是旧版" + id;
        }
    }
    [RoutePrefix("api/v2/Person")]
    public class PersonV2Controller : ApiController
    {
        [Route("{id}")]
        public string Get(int id)
        {
            return "我是V2版" + id;
        }
    }
}

处理方法2(推荐):自定义IHttpControllerSelector

第一步:根据版本号配置路由

namespace WebApi
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //根据版本号来注册路由:第v1版本的请求地址:http://localhost:14483/api/v1/Person/Get

            config.Routes.MapHttpRoute(
                name: "DefaultApiv1",
                routeTemplate: "api/v1/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //根据版本号来注册路由:第v2版本的请求地址:http://localhost:14483/api/v2/Person/Get
            config.Routes.MapHttpRoute(
                name: "DefaultApiv2",
                routeTemplate: "api/v2/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //将自定义的VersionControllerSelector注册到系统中
            config.Services.Replace(typeof(IHttpControllerSelector),new VersionControllerSelector(config));
        }
    }
}

第二步:创建一个VersionControllerSelector.cs类

namespace WebApi.App_Start
{
    public class VersionControllerSelector : DefaultHttpControllerSelector
    {
        private HttpConfiguration _config;
        IDictionary<string, HttpControllerDescriptor> controllers = null;//缓存用
        public VersionControllerSelector(HttpConfiguration config) : base(config)
        {
            _config = config;
        }

        //设计就是返回HttpControllerDesriptor的过程
        public override System.Web.Http.Controllers.HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            //获取所有的controller键值集合
            if (controllers == null)
            {
                GetControllerMapping();
            }
            //获取路由数据
            var routeData = request.GetRouteData();
            //从路由中获取当前controller的名称
            var controllerName = (string)routeData.Values["controller"];
            //从url中获取到版本号
            string verNum = Regex.Match(request.RequestUri.PathAndQuery, @"api/v(\d+)").Groups[1].Value;
            string key = controllerName + "v" + verNum;//获取Personv2
            if (controllers.ContainsKey(key))//获取HttpControllerDescriptor
            {
                return controllers[key];
            }
            else
            {
                return null;
            }
        }

        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,
                    @"WebApi.Controllers.v(\d+)");
                    if (match.Success)
                    {
                        string verNum = match.Groups[1].Value;//获取版本号
                        string ctrlName = Regex.Match(ctrlType.Name, "(.+)Controller").Groups[1].Value;//从PersonController中拿到Person
                        string key = ctrlName + "v" + verNum;//Personv2为key
                        dict[key] = new HttpControllerDescriptor(_config, ctrlName, ctrlType);
                    }
                }
            }
            controllers = dict;//因为项目启动的时候就会调用GetControllerMapping这个方法,这个方法主要是就获取所有的控制器,所以既然项目开始启动的时候就已经调用过这个方法了,已经获取到了所有的控制器了,为了避免我们在重新SelectController方法的时候二次调用,这里把已经取到的控制器字典缓存起来。
            return dict;
        }
    }
}

第三步:在WebApiConfig.cs中 将以下代码加入到Register方法最后面

//将自定义的VersionControllerSelector注册到系统中
config.Services.Replace(typeof(IHttpControllerSelector),new VersionControllerSelector(config));

第四步: 在WebApi项目中的Controllers文件下分别创建v1和v2两个文件夹。 并在两个文件中分别创建一个PersonController.cs控制器

例如:v1文件夹下的PersonController.cs控制器

namespace WebApi.Controllers.v1
{
    public class PersonController : ApiController
    {
        public string Get()
        {
            return "我是v1";
        }
    }
}

v2文件夹下的PersonController.cs控制器

namespace WebApi.Controllers.v2
{
    public class PersonController : ApiController
    {
        public string Get()
        {
            return "我是v2";
        }
    }
}

第五步:请求调用

v1版本的App发起请求的URL是这样的:http://localhost:14483/api/v1/Person/Get

v2版本的App发起请求的URL是这样的:http://localhost:14483/api/v2/Person/Get

这样就达到了不同版本的APP在请求同名的控制器的时候,就会自动调用对应版本的名称空间下的同名控制器


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐