现在的开发大部分都是前后端分离的模式了,后端提供接口,前端调用接口。后端提供了接口,需要对接口进行测试,之前都是使用浏览器开发者工具,或者写单元测试,再或者直接使用Postman,但是现在这些都已经out了。后端提供了接口,如何跟前端配合说明接口的性质,参数,验证情况?这也是一个问题。有没有一种工具可以根据后端的接口自动生成接口文档,说明接口的性质,参数等信息,又能提供接口调用等相关功能呢?

  答案是有的。Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。而作为.net core开发,Swashbuckle是swagger应用的首选!本文旨在介绍Swashbuckle的一些常见功能,以满足大部分开发的需要!

  本文旨在介绍Swashbuckle的一般用法以及一些常用方法,让读者读完之后对Swashbuckle的用法有个最基本的理解,可满足绝大部分需求的需要,比如认证问题、虚拟路劲问题,返回值格式问题等等

  如果对Swashbuckle源码感兴趣,可以去github上pull下来看看  

  github中Swashbuckle.AspNetCore源码地址:https://github.com/domaindrivendev/Swashbuckle.AspNetCore

  

  一、一般用法

   注:这里一般用法的Demo源码已上传到百度云:https://pan.baidu.com/s/1Z4Z9H9nto_CbNiAZIxpFFQ (提取码:pa8s ),下面第二、三部分的功能可在Demo源码基础上去尝试。

  创建一个.net core项目(这里采用的是.net core3.1),然后使用nuget安装Swashbuckle.AspNetCore,建议安装5.0以上版本,因为swagger3.0开始已经加入到OpenApi项目中,因此Swashbuckle新旧版本用法还是有一些差异的。

  比如,我们一个Home控制器:  

    /// <summary>/// 测试接口/// </summary>[ApiController][Route("[controller]")]public class HomeController : ControllerBase{/// <summary>/// Hello World/// </summary>/// <returns>输出Hello World</returns>[HttpGet]public string Get(){return "Hello World";}}

  接口修改Startup,在ConfigureServices和Configure方法中添加服务和中间件  

    public void ConfigureServices(IServiceCollection services){...services.AddSwaggerGen(options =>{options.SwaggerDoc("v1", new OpenApiInfo(){Version = "v0.0.1",Title = "swagger测试项目",Description = $"接口文档说明",Contact = new OpenApiContact(){Name = "zhangsan",Email = "xxx@qq.com",Url = null}});});...}
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){...app.UseSwagger();app.UseSwaggerUI(options =>{options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");});...}

  然后运行项目,输入http://localhost:5000/swagger,得到接口文档页面:

  

  点击Try it out可以直接调用接口。

  这里,发现接口没有注解说明,这不太友好,而Swashbuckle的接口可以从代码注释中获取,也可以使用代码说明,我们做开发的当然想直接从注释获取啦。

  但是另一方面,因为注释在代码编译时会被过滤掉,因此我们需要在项目中生成注释文件,然后让程序加载注释文件,操作如下:

  右键项目=》切换到生成(Build),在最下面输出输出中勾选【XML文档文件】,同时,在错误警告的取消显示警告中添加1591代码:

  

  生成当前项目时会将项目中所有的注释打包到这个文件中。

  然后修改ConfigureServices:  

    public void ConfigureServices(IServiceCollection services){...services.AddSwaggerGen(options =>{options.SwaggerDoc("v1", new OpenApiInfo(){Version = "v0.0.1",Title = "swagger测试项目",Description = $"接口文档说明",Contact = new OpenApiContact(){Name = "zhangsan",Email = "xxx@qq.com",Url = null}});options.IncludeXmlComments("SwashbuckleDemo.xml", true);});...}

  上面使用IncludeXmlComments方法加载注释,第二个参数true表示注释文件包含了控制器的注释,如果不包含控制器注释(如引用的其他类库),可以将它置为false

  注意上面的xml文件要与它对应的dll文件放到同目录,如果不在同一目录,需要自行指定目录,如果找不到文件,可能会抛出异常!

  另外,如果项目引用的其他项目,可以将其他项目也生成xml注释文件,然后使用IncludeXmlComments方法加载,从而避免部分接口信息无注解情况

  运行后可以得到接口的注释:

  

  接着,既然是提供接口,没有认证怎么行,比如,Home控制器下还有一个Post接口,但是接口需要认证,比如JwtBearer认证:  

    /// <summary>/// 测试接口/// </summary>[ApiController][Route("[controller]")]public class HomeController : ControllerBase{.../// <summary>/// 使用认证获取数据/// </summary>/// <returns>返回数据</returns>[HttpPost, Authorize]public string Post(){return "这是认证后的数据";}}

  为了接口能使用认证,修改Startup的ConfigureServices:  

    public void ConfigureServices(IServiceCollection services){...services.AddSwaggerGen(options =>{options.SwaggerDoc("v1", new OpenApiInfo(){Version = "v0.0.1",Title = "swagger测试项目",Description = $"接口文档说明",Contact = new OpenApiContact(){Name = "zhangsan",Email = "xxx@qq.com",Url = null}});options.IncludeXmlComments("SwashbuckleDemo.xml", true);//第二个参数true表示注释文件包含了控制器的注释//定义JwtBearer认证方式一options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme(){Description = "这是方式一(直接在输入框中输入认证信息,不需要在开头添加Bearer)",Name = "Authorization",//jwt默认的参数名称In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)Type = SecuritySchemeType.Http,Scheme = "bearer"});//定义JwtBearer认证方式二//options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme()//{//    Description = "这是方式二(JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格))",//    Name = "Authorization",//jwt默认的参数名称//    In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)//    Type = SecuritySchemeType.ApiKey//});//声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致var scheme = new OpenApiSecurityScheme(){Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" }};//注册全局认证(所有的接口都可以使用认证)options.AddSecurityRequirement(new OpenApiSecurityRequirement(){[scheme] = new string[0]});});...}

  程序运行后效果如下:  

  

  上面说了,添加JwtBearer认证有两种方式,两种方式的区别如下:

     

  到这里应该就已经满足大部分需求的用法了,这也是网上很容易就能搜索到的,接下来介绍的是一些常用到的方法。

  

  服务注入(AddSwaggerGen)

  前面介绍到,Swashbuckle的服务注入是在ConfigureServices中使用拓展方法AddSwaggerGen实现的

    services.AddSwaggerGen(options =>{//使用options注入服务});    

  确切的说swagger的服务注入是使用SwaggerGenOptions来实现的,下面主要介绍SwaggerGenOptions的一些常用的方法:

  SwaggerDoc

  SwaggerDoc主要用来声明一个文档,上面的例子中声明了一个名称为v1的接口文档,当然,我们可以声明多个接口文档,比如按开发版本进行声明:  

    options.SwaggerDoc("v1", new OpenApiInfo(){Version = "v0.0.1",Title = "项目v0.0.1",Description = $"接口文档说明v0.0.1",Contact = new OpenApiContact(){Name = "zhangsan",Email = "xxx@qq.com",Url = null}});options.SwaggerDoc("v2", new OpenApiInfo(){Version = "v0.0.2",Title = "项目v0.0.2",Description = $"接口文档说明v0.0.2",Contact = new OpenApiContact(){Name = "lisi",Email = "xxxx@qq.com",Url = null}});...

  开发过程中,可以将接口文档名称设置成枚举或者常量值,以方便文档名的使用。

  至于上面OpenApiInfo声明的各参数,其实就是要在SwaggerUI页面上展示出来的,读者可自行测试一下,这里不过多说明,只是顺带提一下Description属性,这个是一个介绍文档接口的简介,但是这个属性是支持html展示的,也就是说可以生成一些html代码放到Description属性中。

  声明多个文档,可以将接口进行归类,不然一个项目几百个接口,查看起来也不方便,而将要接口归属某个文档,我们可以使ApiExplorerSettingsAttribute指定GroupName来指定,如:  

    /// <summary>/// 未使用ApiExplorerSettings特性,表名属于每一个swagger文档/// </summary>/// <returns>结果</returns>[HttpGet("All")]public string All(){return "All";}/// <summary>/// 使用ApiExplorerSettings特性表名该接口属于swagger文档v1/// </summary>/// <returns>Get结果</returns>[HttpGet][ApiExplorerSettings(GroupName = "v1")]public string Get(){return "Get";}/// <summary>/// 使用ApiExplorerSettings特性表名该接口属于swagger文档v2/// </summary>/// <returns>Post结果</returns>[HttpPost][ApiExplorerSettings(GroupName = "v2")]public string Post(){return "Post";}

  因为我们现在有两个接口文档了,想要在swaggerUI中看得到,还需要在中间件中添加相关文件的swagger.json文件的入口:  

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){...app.UseSwagger();app.UseSwaggerUI(options =>{options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");options.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");});...}

  运行项目后:

  

  

  上面使用ApiExplorerSettingsAttribute的GroupName属性指定归属的swagger文档(GroupName需要设置成上面SwaggerDoc声明的文档的名称),如果不使用ApiExplorerSettingsAttribute,那么接口将属于所有的swagger文档,上面的例子可以看到/Home/All接口既属于v1也属于v2。

  另外ApiExplorerSettingsAttribute还有个IgnoreApi属性,如果设置成true,将不会在swagger页面展示该接口。

  但是接口一个个的去添加ApiExplorerSettingsAttribute,是不是有点繁琐了?没事,我们可以采用Convertion实现,主要是IActionModelConvention和IControllerModelConvention两个:

  IActionModelConvention方式:  

    public class GroupNameActionModelConvention : IActionModelConvention{public void Apply(ActionModel action){if (action.Controller.ControllerName == "Home"){if (action.ActionName == "Get"){action.ApiExplorer.GroupName = "v1";action.ApiExplorer.IsVisible = true;}else if (action.ActionName == "Post"){action.ApiExplorer.GroupName = "v2";action.ApiExplorer.IsVisible = true;}}}}

  然后在ConfigureService中使用:  

    services.AddControllers(options =>{options.Conventions.Add(new GroupNameActionModelConvention());});

  或者使用IControllerModelConvention方式:  

    public class GroupNameControllerModelConvention : IControllerModelConvention{public void Apply(ControllerModel controller){if (controller.ControllerName == "Home"){foreach (var action in controller.Actions){if (action.ActionName == "Get"){action.ApiExplorer.GroupName = "v1";action.ApiExplorer.IsVisible = true;}else if (action.ActionName == "Post"){action.ApiExplorer.GroupName = "v2";action.ApiExplorer.IsVisible = true;}}}}}

  然后在ConfigureService中使用:  

    services.AddControllers(options =>{options.Conventions.Add(new GroupNameControllerModelConvention());});

  这两种方式实现的效果和使用ApiExplorerSettingsAttribute是一样的,细心的朋友可能会注意,action.ApiExplorer.GroupName与ApiExplorerSettingsAttribute.GroupName是对应的,action.ApiExplorer.IsVisible则与ApiExplorerSettingsAttribute.IgnoreApi是对应的  

  IncludeXmlComments

  IncludeXmlComments是用于加载注释文件,Swashbuckle会从注释文件中去获取接口的注解,接口参数说明以及接口返回的参数说明等信息,这个在上面的一般用法中已经介绍了,这里不再重复说明

  IgnoreObsoleteActions

  IgnoreObsoleteActions表示过滤掉ObsoleteAttribute属性声明的接口,也就是说不会在SwaggerUI中显示接口了,ObsoleteAttribute修饰的接口表示接口已过期,尽可能不要再使用。

  方法调用等价于:  

    options.SwaggerGeneratorOptions.IgnoreObsoleteActions = true;

  IgnoreObsoleteProperties

  IgnoreObsoleteProperties的作用类似于IgnoreObsoleteActions,只不过IgnoreObsoleteActions是作用于接口,而IgnoreObsoleteProperties作用于接口的请求实体和响应实体参数中的属性。

  方法调用等价于:  

    options.SchemaGeneratorOptions.IgnoreObsoleteProperties = true;

  OrderActionsBy

  OrderActionsBy用于同一组接口(可以理解为同一控制器下的接口)的排序,默认情况下,一般都是按接口所在类的位置进行排序(源码中是按控制器名称排序,但是同一个控制器中的接口是一样的)。

  比如上面的例子中,我们可以修改成按接口路由长度排序:  

    options.OrderActionsBy(apiDescription => apiDescription.RelativePath.Length.ToString());

  运行后Get接口和Post接口就在All接口前面了:

  

  需要注意的是,OrderActionsBy提供的排序只有升序,其实也就是调用IEnumerable<ApiDescription>的OrderBy方法,虽然不理解为什么只有升序,但降序也是可以采用这个升序实现的,将就着用吧。

  CustomSchemaIds

  CustomSchemaIds方法用于自定义SchemaId,Swashbuckle中的每个Schema都有唯一的Id,框架会使用这个Id匹配引用类型,因此这个Id不能重复。

  默认情况下,这个Id是根据类名得到的(不包含命名空间),因此,当我们有两个相同名称的类时,Swashbuckle就会报错:  

    System.InvalidOperationException: Can't use schemaId "$XXXXX" for type "$XXXX.XXXX". The same schemaId is already used for type "$XXXX.XXXX.XXXX"

  就是类似上面的异常,一般时候我们都得去改类名,有点不爽,这时就可以使用这个方法自己自定义实现SchemaId的获取,比如,我们自定义实现使用类名的全限定名(包含命名空间)来生成SchemaId,上面的异常就没有了:   

    options.CustomSchemaIds(CustomSchemaIdSelector);string CustomSchemaIdSelector(Type modelType){if (!modelType.IsConstructedGenericType) return modelType.FullName.Replace("[]", "Array");var prefix = modelType.GetGenericArguments().Select(genericArg => CustomSchemaIdSelector(genericArg)).Aggregate((previous, current) => previous + current);return prefix + modelType.FullName.Split('`').First();}

   TagActionsBy

  Tag是标签组,也就是将接口做分类的一个概念。

  TagActionsBy用于获取一个接口所在的标签分组,默认的接口标签分组是控制器名,也就是接口被分在它所属的控制器下面,我们可以改成按请求方法进行分组  

    options.TagActionsBy(apiDescription => new string[] { apiDescription.HttpMethod});

  运行后:

  

  注意到,上面还有一个Home空标签,如果不想要这个空标签,可以将它的注释去掉,(不明白为什么Swashbuckle为什么空标签也要显示出来,难道是因为作者想着只要有东西能展示,就应该显示出来?)

  MapType

    MapType用于自定义类型结构(Schema)的生成,Schema指的是接口参数和返回值等的结构信息。

  比如,我有一个获取用户信息的接口:  

    /// <summary>/// 获取用户/// </summary>/// <returns>用户信息</returns>[HttpGet("GetUser")]public User GetUser(int id){//这里根据Id获取用户信息return new User(){Name = "张三"};}

  其中User是自己定义的一个实体   

    /// <summary>/// 用户信息/// </summary>public class User{/// <summary>/// 用户名称/// </summary>public string Name { get; set; }/// <summary>/// 用户密码/// </summary>public string Password { get; set; }/// <summary>/// 手机号码/// </summary>public string Phone { get; set; }/// <summary>/// 工作/// </summary>public string Job { get; set; }}

  默认情况下,swagger生成的结构是json格式:

  

  通过MapType方法,可以修改User生成的架构,比如修改成字符串类型:  

    options.MapType<User>(() =>{return new OpenApiSchema() {Type= "string"};                    });

  运行后显示:

  

   AddServer

  Server指的是接口访问的域名和前缀(虚拟路径),以方便访问不同地址的接口(注意设置跨域).

  AddServer用于全局的添加接口域名和前缀(虚拟路径)部分信息,默认情况下,如果我们在SwaggerUi页面使用Try it out去调用接口时,默认使用的是当前swaggerUI页面所在的地址域名信息:

  

  而AddServer方法运行我们添加其他的地址域名,比如:  

    options.AddServer(new OpenApiServer() { Url = "http://localhost:5000", Description = "地址1" });options.AddServer(new OpenApiServer() { Url = "http://127.0.0.1:5001", Description = "地址2" });//192.168.28.213是我本地IPoptions.AddServer(new OpenApiServer() { Url = "http://192.168.28.213:5002", Description = "地址3" });

  我分别在上面3个端口开启程序,运行后:

  

  注意:如果读者本地访问不到,看看自己程序是否有监听这三个地址,而且记得要设置跨域,否则会导致请求失败:  

   public void ConfigureServices(IServiceCollection services){...services.AddCors();...}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){...app.UseCors(builder =>{builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();});...}

  在开发过程中,我们的程序可能会发布到不同的环境,比如本地开发环境,测试环境,预生产环境等等,因此,我们可以使用AddServer方法将不同环境的地址配置上去就能直接实现调用了。

  在项目部署时,可能会涉及到虚拟目录之类的东西,比如,使用IIS部署时,可能会给项目加一层虚拟路径:

  

  或者使用nginx做一层反向代理:

  

  这个时候虽然可以使用http://ip:port/Swashbuckle/swagger/index.html访问到swaggerUI,但是此时可能会报错 Not Found /swagger/v1/swagger.json:

  

  这是因为加了虚拟路径,而swagger并不知道,所以再通过/swagger/v1/swagger.json去获取接口架构信息当然会报404了,我们可以改下Swagger中间件:  

    app.UseSwagger();app.UseSwaggerUI(options =>{options.SwaggerEndpoint("/Swashbuckle/swagger/v1/swagger.json", "v1");options.SwaggerEndpoint("/Swashbuckle/swagger/v2/swagger.json", "v2");});

  再使用虚拟路径就可以访问到SwaggerUI页面了,但是问题还是有的,因为所有接口都没有加虚拟路径,上面说道,swagger调用接口默认是使用SwaggerUI页面的地址+接口路径去访问的,这就会少了虚拟路径,访问自然就变成了404:

  

  这个时候就可以调用AddServer方法去添加虚拟路径了:  

    //注意下面的端口,已经变了options.AddServer(new OpenApiServer() { Url = "http://localhost:90/Swashbuckle", Description = "地址1" });options.AddServer(new OpenApiServer() { Url = "http://127.0.0.1:90/Swashbuckle", Description = "地址2" });//192.168.28.213是我本地IPoptions.AddServer(new OpenApiServer() { Url = "http://192.168.28.213:90/Swashbuckle", Description = "地址3" });

  部署运行后就可以访问了:

  

  一般的,开发过程中,我们可以把这个虚拟路径做成配置,在然后从配置读取即可。

  注:我记得Swashbuckle在swagger2.0的版本中SwaggerDocument中有个BasePath,可以很轻松的设置虚拟路径,但是在swagger3+之后把这个属性删除了,不知道什么原因

  AddSecurityDefinition

  AddSecurityDefinition用于声明一个安全认证,注意,只是声明,并未指定接口必须要使用认证,比如声明JwtBearer认证方式:  

    //定义JwtBearer认证方式一options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme(){Description = "这是方式一(直接在输入框中输入认证信息,不需要在开头添加Bearer)",Name = "Authorization",//jwt默认的参数名称In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)Type = SecuritySchemeType.Http,Scheme = "bearer"});

  AddSecurityDefinition方法需要提供一个认证名以及一个OpenApiSecurityScheme对象,而这个OpenApiSecurityScheme对象就是描述的认证信息,常用的有:  

   Type:表示认证方式,有ApiKey,Http,OAuth2,OpenIdConnect四种,其中ApiKey是用的最多的。Description:认证的描述Name:携带认证信息的参数名,比如Jwt默认是AuthorizationIn:表示认证信息发在Http请求的哪个位置Scheme:认证主题,只对Type=Http生效,只能是basic和bearerBearerFormat::Bearer认证的数据格式,默认为Bearer Token(中间有一个空格)Flows:OAuth认证相关设置,比如认证方式等等OpenIdConnectUrl:使用OAuth认证和OpenIdConnect认证的配置发现地址Extensions:认证的其他拓展,如OpenIdConnect的Scope等等Reference:关联认证

   这些属性中,最重要的当属Type,它指明了认证的方式,用通俗的话讲:

  ApiKey表示就是提供一个框,你填值之后调用接口,会将填的值与Name属性指定的值组成一个键值对,放在In参数指定的位置通过http传送到后台。

  Http也是提供了一个框,填值之后调用接口,会将填的值按照Scheme指定的方式进行处理,再和Name属性组成一个键值对,放在In参数指定的位置通过http传送到后台。这也就解释了为什么Bearer认证可以有两种方式。

  OAuth2,OpenIdConnect需要提供账号等信息,然后去远程服务进行授权,一般使用Swagger都不推荐使用这种方式,因为比较复杂,而且授权后的信息也可以通过ApiKey方式传送到后台。

  再举个例子,比如我们使用Cookie认证:  

    options.AddSecurityDefinition("Cookies", new OpenApiSecurityScheme(){Description = "这是Cookie认证方式",Name = "Cookies",//这个是Cookie名 In = ParameterLocation.Cookie,//信息保存在Cookie中Type = SecuritySchemeType.ApiKey});

  注:如果将信息放在Cookie,那么在SwaggerUI中调用接口时,认证信息可能不会被携带到后台,因为浏览器不允许你自己操作Cookie,因此在发送请求时会过滤掉你自己设置的Cookie,但是SwaggerUI页面调用生成的Curl命令语句是可以成功访问的

   好了,言归正传,当添加了上面JwtBearer认证方式后,这时SwaggerUI多了一个认证的地方:

  

  但是这时调用接口并不需要认证信息,因为还没有指定哪些接口需要认证信息

  AddSecurityRequirement

   AddSecurityDefinition仅仅是声明已一个认证,不一定要对接口用,而AddSecurityRequirement是将声明的认证作用于所有接口(AddSecurityRequirement好像可以声明和引用一起实现),比如将上面的JwtBearer认证作用于所有接口:  

    //声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致var scheme = new OpenApiSecurityScheme(){Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" }};//注册全局认证(所有的接口都可以使用认证)options.AddSecurityRequirement(new OpenApiSecurityRequirement(){[scheme] = new string[0]});

  运行后,发现所有接口后面多了一个锁,表明此接口需要认证信息:

  

 AddSecurityRequirement调用需要一个OpenApiSecurityRequirement对象,他其实是一个字典型,也就是说可以给接口添加多种认证方式,而它的键是OpenApiSecurityScheme对象,比如上面的例子中将新定义的OpenApiSecurityScheme关联到已经声明的认证上,而值是一个字符串数组,一般指的是OpenIdConnect的Scope。

  需要注意的是,AddSecurityRequirement声明的作用是对全部的接口生效,也就是说所有接口后面都会加锁,但这并不影响我们接口的调用,毕竟调用逻辑还是由后台代码决定的,但是这里加锁就容易让人误导以为都需要认证。

  DocumentFilter

  document顾名思义,当然指的就是swagger文档了。

  DocumentFilter是文档过滤器,它是在获取swagger文档接口,返回结果前调用,也就是请求swagger.json时调用,它允许我们对即将返回的swagger文档信息做调整,比如上面的例子中添加的全局认证方式和AddSecurityRequirement添加的效果是一样的:  

    public class MyDocumentFilter : IDocumentFilter{public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context){//声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致var scheme = new OpenApiSecurityScheme(){Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" }};//注册全局认证(所有的接口都可以使用认证)swaggerDoc.SecurityRequirements.Add(new OpenApiSecurityRequirement(){[scheme] = new string[0]});}}

  然后使用DocumentFilter方法添加过滤器:  

    options.DocumentFilter<MyDocumentFilter>();

  DocumentFilter方法需要提供一个实现了IDocumentFilter接口的Apply方法的类型和它实例化时所需要的的参数,而IDocumentFilter的Apply方法提供了OpenApiDocument和DocumentFilterContext两个参数,DocumentFilterContext参数则包含了当前文件接口方法的信息,比如调用的接口的Action方法和Action的描述(如路由等)。而OpenApiDocument即包含当前请求的接口文档信息,它包含的属性全部都是全局性的, 这样我们可以像上面添加认证一样去添加全局配置,比如,如果不使用AddServer方法,我们可以使用DocumentFilter去添加:  

    public class MyDocumentFilter : IDocumentFilter{public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context){swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://localhost:90", Description = "地址1" });swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://127.0.0.1:90", Description = "地址2" });//192.168.28.213是我本地IPswaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://192.168.28.213:90", Description = "地址3" });}}

  记得使用DocumentFilter添加过滤器。

  再比如,上面我们对接口进行了swagger文档分类使用的是ApiExplorerSettingsAttribute,如果不想对每个接口使用ApiExplorerSettingsAttribute,我们可以使用DocumentFilter来实现,先创建一个类实现IDocumentFilter接口: 

    public class GroupNameDocumentFilter : IDocumentFilter{string documentName;string[] actions;public GroupNameDocumentFilter(string documentName, params string[] actions){this.documentName = documentName;this.actions = actions;}public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context){foreach (var apiDescription in context.ApiDescriptions){if (actions.Contains(apiDescription.ActionDescriptor.RouteValues["action"])){apiDescription.GroupName = documentName;}}}}

  然后使用DocumentFilter添加过滤器: 

    //All和Get接口属于文档v1options.DocumentFilter<GroupNameDocumentFilter>(new object[] { "v1", new string[] { nameof(HomeController.Get) } });//All和Post接口属于v2options.DocumentFilter<GroupNameDocumentFilter>(new object[] { "v2", new string[] { nameof(HomeController.Post) } });

  然后取消上面Get方法和Post方法的ApiExplorerSettings特性,这样实现的效果和上面直接使用ApiExplorerSettings特性修饰的效果是相似的。

  这里说相似并非一致,是因为上面的GroupNameDocumentFilter是在第一次获取swagger.json时执行设置GroupName,也就是说第一次获取swagger.json会获取到所有的接口,所以一般也不会采用这种方法,而是采用上面介绍的使用IActionModelConvention和IControllerModelConvention来实现。

  OperationFilter

  什么是Operation?Operation可以简单的理解为一个操作,因为swagger是根据项目中的接口,自动生成接口文档,就自然需要对每个接口进行解析,接口路由是什么,接口需要什么参数,接口返回什么数据等等,而对每个接口的解析就可以视为一个Operation。

  OperationFilter是操作过滤器,这个方法需要一个实现类IOperationFilter接口的类型,而它的第二个参数arguments是这个类型实例化时传入的参数。

  OperationFilter允许我们对已经生成的接口进行修改,比如可以添加参数,修改参数类型等等。

  需要注意的是,OperationFilter在获取swagger文档接口时调用,也就是请求swagger.json时调用,而且只对属于当前请求接口文档的接口进行过滤调用。  

  比如我们有一个Operation过滤器:

    public class MyOperationFilter : IOperationFilter{string documentName;public MyOperationFilter(string documentName){this.documentName = documentName;}public void Apply(OpenApiOperation operation, OperationFilterContext context){//过滤处理}}

  接着调用SwaggerGenOptions的OperationFilter方法添加  

    options.OperationFilter<MyOperationFilter>(new object[] { "v1" });

  上面的过滤器实例化需要一个参数documentName,所以在OperationFilter方法中有一个参数。

  这个接口只会对当前请求的接口文档进行调用,也就是说,如果我们请求的是swagger文档v1,也就是请求/swagger/v1/swagger.json时,这个过滤器会对All方法和Get方法执行,如果请求的是swagger文档v2,也就是请求/swagger/v2/swagger.json时,这个过滤器会对All方法和Post方法进行调用。自定义的OperationFilter需要实现IOperationFilter的Apply接口方法,而Apply方法有两个参数:OpenApiOperation和OperationFilterContext,同样的,OpenApiOperation包含了和当前接口相关的信息,比如认证情况,所属的标签,还可以自定义的自己的Servers。而OperationFilterContext则包换了接口方法的的相关引用。

  OperationFilter是用的比较多的方法了,比如上面的全局认证,因为直接调用AddSecurityRequirement添加的是全局认证,但是项目中可能部分接口不需要认证,这时我们就可以写一个OperationFilter对每一个接口进行判断了:  

    public class ResponsesOperationFilter : IOperationFilter{public void Apply(OpenApiOperation operation, OperationFilterContext context){var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true).Union(context.MethodInfo.GetCustomAttributes(true)).OfType<AuthorizeAttribute>();var list = new List<OpenApiSecurityRequirement>();if (authAttributes.Any() && !context.MethodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any()){operation.Responses["401"] = new OpenApiResponse { Description = "Unauthorized" };//operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });//声明一个Scheme,注意下面的Id要和AddSecurityDefinition中的参数name一致var scheme = new OpenApiSecurityScheme(){Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" }};//注册全局认证(所有的接口都可以使用认证)operation.Security = new List<OpenApiSecurityRequirement>(){new OpenApiSecurityRequirement(){[scheme] = new string[0]}};}}}

  然后使用OperationFilter添加这个过滤器:  

    options.OperationFilter<ResponsesOperationFilter>();

  现在可以测试一下了,我们将上面的All接口使用Authorize特性添加认证

    /// <summary>/// 未使用ApiExplorerSettings特性,表名属于每一个swagger文档/// </summary>/// <returns>结果</returns>[HttpGet("All"), Authorize]public string All(){return "All";}

  然后运行项目得到:

  

  再比如,我们一般写接口,都会对返回的数据做一个规范,比如每个接口都会有响应代码,响应信息等等,而程序中我们是通过过滤器去实现的,所以接口都是直接返回数据,但是我们的swagger不知道,比如上面我们的测试接口返回的都是string类型,所以页面上也是展示string类型没错:

  

  假如我们添加了过滤器对结果进行了一个处理,结果不在是string类型了,这个时候我们就可以使用OperationFilter做一个调整了:  

    public class MyOperationFilter : IOperationFilter{public void Apply(OpenApiOperation operation, OperationFilterContext context){foreach (var key in operation.Responses.Keys){var content = operation.Responses[key].Content;foreach (var mediaTypeKey in content.Keys){var mediaType = content[mediaTypeKey];var schema = new OpenApiSchema();schema.Type = "object";schema.Properties = new Dictionary<string, OpenApiSchema>(){["code"] = new OpenApiSchema() { Type = "integer" },["message"] = new OpenApiSchema() { Type = "string" },["error"] = new OpenApiSchema(){Type = "object",Properties = new Dictionary<string, OpenApiSchema>(){["message"] = new OpenApiSchema() { Type = "string" },["stackTrace"] = new OpenApiSchema() { Type = "string" }}},["result"] = mediaType.Schema};mediaType.Schema = schema;}}}}

  记得使用OperationFilter添加过滤器:  

    options.OperationFilter<MyOperationFilter>();

  显示效果如下:

  

  RequestBodyFilter

  RequestBody理所当然的就是请求体了,一般指的就是Post请求,RequestBodyFilter就是允许我们对请求体的信息作出调整,同样的,它是在获取Swagger.json文档时调用,而且只对那些有请求体的接口才会执行。

  RequestBodyFilter的用法类似DocumentFilter和OperationFilter,一般也不会去修改请求体的默认行为,因为它可能导致请求失败,所以一般不常用,这里就不介绍了

  ParameterFilter

  Parameter指的是接口的参数,而ParameterFilter当然就是允许我们对参数的结构信息作出调整了,同样的,它是在获取Swagger.json文档时调用,而且只对那些参数的接口才会执行。

  比如,我们有这么一个接口:  

    /// <summary>/// 有参数接口/// </summary>/// <returns></returns>[HttpGet("GetPara")]public string GetPara(string para="default"){return $"para is {para},but para from header is {Request.Headers["para"]}";}

  然后我们可以使用ParameterFilter修改上面para参数在http请求中的位置,比如将它放在请求头中:  

    public class MyParameterFilter : IParameterFilter{public void Apply(OpenApiParameter parameter, ParameterFilterContext context){if (context.ParameterInfo.Name == "para"){parameter.In = ParameterLocation.Header;}}}

  然后使用ParameterFilter方法添加过滤器:  

    options.ParameterFilter<MyParameterFilter>();

  运行后:

  

  不过一般不会使用ParameterFilter去修改参数的默认行为,因为这可能会导致接口调用失败。

  SchemaFilter

  Schema指的是结构,一般指的是接口请求参数和响应返回的参数结构,比如我们想将所有的int类型换成string类型:  

    public class MySchemaFilter : ISchemaFilter{public void Apply(OpenApiSchema schema, SchemaFilterContext context){if (context.Type == typeof(int)){schema.Type = "string";}}}

  加入有接口:  

    /// <summary>/// 测试接口/// </summary>/// <returns></returns>[HttpGet("Get")]public int Get(int id){return 1;}

  运行后所有的int参数在swaggerUI上都会显示为string 类型:  

  

  其他方法

  其他方法就不准备介绍了,比如:

  DescribeAllEnumsAsStrings方法表示在将枚举类型解释成字符串名称而不是默认的整形数字

  DescribeAllParametersInCamelCase方法表示将参数使用驼峰命名法处理

  等等这些方法都用的比较少,而且这些都比较简单,感兴趣的可以看看源码学习

  三、添加Swagger中间件(UseSwagger,UseSwaggerUI)

  细心地朋友应该注意到,在上面的例子中,添加Swagger中间件其实有两个,分别是UseSwagger和UseSwaggerUI两个方法:

  UseSwagger:添加Swagger中间件,主要用于拦截swagger.json请求,从而可以获取返回所需的接口架构信息

  UseSwaggerUI:添加SwaggerUI中间件,主要用于拦截swagger/index.html页面请求,返回页面给前端

  整个swagger页面访问流程如下:

  1、浏览器输入swaggerUI页面地址,比如:http://localhost:5000/swagger/index.html,这个地址是可配置的

  2、请求被SwaggerUI中间件拦截,然后返回页面,这个页面是嵌入的资源文件,也可以设置成外部自己的页面文件(使用外部静态文件拦截)

  3、页面接收到Swagger的Index页面后,会根据SwaggerUI中间件中使用SwaggerEndpoint方法设置的文档列表,加载第一个文档,也就是获取文档架构信息swagger.json

  4、浏览器请求的swagger.json被Swagger中间件拦截,然后解析属于请求文档的所有接口,并最终返回一串json格式的数据

  5、浏览器根据接收到的swagger,json数据呈现UI界面

  UseSwagger方法有个包含SwaggerOptions的重载,UseSwaggerUI则有个包含SwaggerUIOptions的重载,两者相辅相成,所以这里在一起介绍这两个方法

  SwaggerOptions

   SwaggerOptions比较简单,就三个属性:

  RouteTemplate

   路由模板,默认值是/swagger/{documentName}/swagger.json,这个属性很重要!而且这个属性中必须包含{documentName}参数。

  上面第3、4步骤已经说到,index.html页面会根据SwaggerUI中间件中使用SwaggerEndpoint方法设置的文档列表,然后使用第一个文档的路由发送一个GET请求,请求会被Swagger中间件中拦截,然后Swagger中间件中会使用RouteTemplate属性去匹配请求路径,然后得到documentName,也就是接口文档名,从而确定要返回哪些接口,所以,这个RouteTemplate一定要配合SwaggerEndpoint中的路由一起使用,要保证通过SwaggerEndpoint方法中的路由能找到documentName。

  比如,如果将RouteTemplate设置成:  

    app.UseSwagger(options =>{options.RouteTemplate = "/{documentName}.json";});

  那么SwaggerEndpoint就得做出相应的调整:  

    app.UseSwaggerUI(options =>{options.SwaggerEndpoint("/v1.json", "v1");options.SwaggerEndpoint("/v2.json", "v2");});

  当然,上面的SwaggerEndpoint方法中的路由可以添加虚拟路径,毕竟虚拟路径会在转发时被处理掉。

  总之,这个属性很重要,尽可能不要修改,然后是上面默认的格式在SwaggerEndpoint方法中声明。

  SerializeAsV2

   表示按Swagger2.0格式序列化生成swagger.json,这个不推荐使用,尽可能的使用新版本的就可以了。

  PreSerializeFilters

  这个属性也是个过滤器,类似于上面介绍的DocumentFilter,在解析完所有接口后得到swaggerDocument之后调用执行,也就是在DocumentFilter,OperationFilter等过滤器之后调用执行。不建议使用这个属性,因为它能实现的功能使用DocumentFilter,OperationFilter等过滤器都能实现。

  SwaggerUIOptions

  SwaggerUIOptions则包含了SwaggerUI页面的一些设置,主要有六个属性:

  RoutePrefix

  设置SwaggerUI的Index页面的地址,默认是swagger,也就是说可以使用http://host:port/swagger可以访问到SwaggerUI页面,如果设置成空字符串,那么久可以使用http://host:port直接访问到SwaggerUI页面了

  IndexStream

  上面解释过,Swagger的UI页面是嵌入的资源文件,默认值是:  

    app.UseSwaggerUI(options =>{options.IndexStream = () => typeof(SwaggerUIOptions).GetTypeInfo().Assembly.GetManifestResourceStream("Swashbuckle.AspNetCore.SwaggerUI.index.html");});

  我们可以修改成自己的页面,比如Hello World:  

    app.UseSwaggerUI(options =>{options.IndexStream = () => new MemoryStream(Encoding.UTF8.GetBytes("Hello World"));});

  DocumentTitle

  这个其实就是html页面的title

  HeadContent

  这个属性是往SwaggerUI页面head标签中添加我们自己的代码,比如引入一些样式文件,或者执行自己的一些脚本代码,比如:  

    app.UseSwaggerUI(options =>{options.HeadContent += $"<script type='text/javascript'>alert('欢迎来到SwaggerUI页面')</script>";});

  然后进入SwaggerUI就会弹出警告框了。

  注意,上面的设置使用的是+=,而不是直接赋值。

  但是一般时候,我们不是直接使用HeadConten属性的,而是使用 SwaggerUIOptions的两个拓展方法去实现:InjectStylesheet和InjectJavascript,这两个拓展方法主要是注入样式和javascript代码:  

    /// <summary>/// Injects additional CSS stylesheets into the index.html page/// </summary>/// <param name="options"></param>/// <param name="path">A path to the stylesheet - i.e. the link "href" attribute</param>/// <param name="media">The target media - i.e. the link "media" attribute</param>public static void InjectStylesheet(this SwaggerUIOptions options, string path, string media = "screen"){var builder = new StringBuilder(options.HeadContent);builder.AppendLine($"<link href='{path}' rel='stylesheet' media='{media}' type='text/css' />");options.HeadContent = builder.ToString();}/// <summary>/// Injects additional Javascript files into the index.html page/// </summary>/// <param name="options"></param>/// <param name="path">A path to the javascript - i.e. the script "src" attribute</param>/// <param name="type">The script type - i.e. the script "type" attribute</param>public static void InjectJavascript(this SwaggerUIOptions options, string path, string type = "text/javascript"){var builder = new StringBuilder(options.HeadContent);builder.AppendLine($"<script src='{path}' type='{type}'></script>");options.HeadContent = builder.ToString();}

  ConfigObject

  其他配置对象,包括之前介绍的SwaggerDocument文档的地址等等。

  OAuthConfigObject

  和OAuth认证有关的配置信息,比如ClientId、ClientSecret等等。

  对于ConfigObject,OAuthConfigObject两个对象,一般都不是直接使用它,而是用SwaggerUIOptions的拓展方法,比如之前一直介绍的SwaggerEndpoint方法,其实就是给ConfigObject的Urls属性增加对象:  

    /// <summary>/// Adds Swagger JSON endpoints. Can be fully-qualified or relative to the UI page/// </summary>/// <param name="options"></param>/// <param name="url">Can be fully qualified or relative to the current host</param>/// <param name="name">The description that appears in the document selector drop-down</param>public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name){var urls = new List<UrlDescriptor>(options.ConfigObject.Urls ?? Enumerable.Empty<UrlDescriptor>());urls.Add(new UrlDescriptor { Url = url, Name = name} );options.ConfigObject.Urls = urls;}

  

  四、总结

  到这里基本上就差不多了,写了这么多该收尾了。

   主要就是记住三点:

  1、服务注入使用AddSwaggerGen方法,主要就是生成接口相关信息,如认证,接口注释等等,还有几种过滤器帮助我们实现自己的需求

  2、中间件注入有两个:UseSwagger和UseSwaggerUI:

     UseSwagger负责返回接口架构信息,返回的是json格式的数据

     UseSwaggerUI负责返回的是页面信息,返回的是html内容

  3、如果涉及到接口生成的,尽可能在AddSwaggerGen中实现,如果涉及到UI页面的,尽可能在UseSwaggerUI中实现

.net core的Swagger接口文档使用教程(二):NSwag

  上一篇介绍了Swashbuckle ,地址:.net core的Swagger接口文档使用教程(一):Swashbuckle

  讲的东西还挺多,怎奈微软还推荐了一个NSwag,那就继续写吧!

  但是和Swashbuckle一样,如果还是按照那样写,东西有点多了,所以这里就偷个懒吧,和Swashbuckle对照的去写,介绍一些常用的东西算了,所以建议看完上一篇再继续这里。

  

  一、一般用法

  注:这里一般用法的Demo源码已上传到百度云:https://pan.baidu.com/s/1Z4Z9H9nto_CbNiAZIxpFFQ (提取码:pa8s ),下面第二、三部分的功能可在Demo源码基础上去尝试。

  创建一个.net core项目(这里采用的是.net core3.1),然后使用nuget安装NSwag.AspNetCore,建议安装最新版本。

  同样的,假如有一个接口:  

    /// <summary>/// 测试接口/// </summary>[ApiController][Route("[controller]")]public class HomeController : ControllerBase{/// <summary>/// Hello World/// </summary>/// <returns>输出Hello World</returns>[HttpGet]public string Get(){return "Hello World";}}

  接口修改Startup,在ConfigureServices和Configure方法中添加服务和中间件  

    public void ConfigureServices(IServiceCollection services){services.AddOpenApiDocument(settings =>{settings.DocumentName = "v1";settings.Version = "v0.0.1";settings.Title = "测试接口项目";settings.Description = "接口文档说明";});...}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){...app.UseOpenApi();app.UseSwaggerUi3();...}

  然后运行项目,输入http://localhost:5000/swagger,得到接口文档页面:

  

  点击Try it out可以直接调用接口。

  同样的,这里的接口没有注解,不太友好,可以和Swashbuckle一样生成xml注释文件加载:

  右键项目=》切换到生成(Build),在最下面输出输出中勾选【XML文档文件】,同时,在错误警告的取消显示警告中添加1591代码:

  

  不过,与Swashbuckle不一样的是,Swashbuckle需要使用IncludeXmlComments方法加载注释文件,如果注释文件不存在,IncludeXmlComments方法还会抛出异常,但是NSwag不需要手动加载,默认xml注释文件和它对应点dll应该放在同一目录且同名才能完成加载!

  按照上面的操作,运行项目后,接口就有注解了:

  

  但是控制器标签栏还是没有注解,这是因为NSwag的控制器标签默认从OpenApiTagAttribute中读取   

    [OpenApiTag("测试标签",Description = "测试接口")]public class HomeController : ControllerBase

  运行后显示:

  

  其实还可以修改这个默认行为,settings有一个UseControllerSummaryAsTagDescription属性,将它设置成 true就可以从xml注释文件中加载描述了:  

    services.AddOpenApiDocument(settings =>{...//可以设置从注释文件加载,但是加载的内容可被OpenApiTagAttribute特性覆盖settings.UseControllerSummaryAsTagDescription = true;});

  运行后显示:

  

  接着是认证,比如JwtBearer认证,这个和Swashbuckle是类似的,只不过拓展方法换成了AddSecurity:  

    public void ConfigureServices(IServiceCollection services){services.AddOpenApiDocument(settings =>{settings.DocumentName = "v1";settings.Version = "v0.0.1";settings.Title = "测试接口项目";settings.Description = "接口文档说明";//可以设置从注释文件加载,但是加载的内容可悲OpenApiTagAttribute特性覆盖settings.UseControllerSummaryAsTagDescription = true;//定义JwtBearer认证方式一settings.AddSecurity("JwtBearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme(){Description = "这是方式一(直接在输入框中输入认证信息,不需要在开头添加Bearer)",Name = "Authorization",//jwt默认的参数名称In = OpenApiSecurityApiKeyLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)Type = OpenApiSecuritySchemeType.Http,Scheme = "bearer"});//定义JwtBearer认证方式二settings.AddSecurity("JwtBearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme(){Description = "这是方式二(JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格))",Name = "Authorization",//jwt默认的参数名称In = OpenApiSecurityApiKeyLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)Type = OpenApiSecuritySchemeType.ApiKey});});...}

  到这里,就是NSwag的一般用法了,可以满足一般的需求了。

  二、服务注入(AddOpenApiDocument和AddSwaggerDocument)

  NSwag注入服务有两个方法:AddOpenApiDocument和AddSwaggerDocument,两者的区别就是架构类型不一样,AddOpenApiDocument的SchemaType使用的是OpenApi3,AddSwaggerDocument的SchemaType使用的是Swagger2:  

    /// <summary>Adds services required for Swagger 2.0 generation (change document settings to generate OpenAPI 3.0).</summary>/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>/// <param name="configure">Configure the document.</param>public static IServiceCollection AddOpenApiDocument(this IServiceCollection serviceCollection, Action<AspNetCoreOpenApiDocumentGeneratorSettings, IServiceProvider> configure = null){return AddSwaggerDocument(serviceCollection, (settings, services) =>{settings.SchemaType = SchemaType.OpenApi3;configure?.Invoke(settings, services);});}/// <summary>Adds services required for Swagger 2.0 generation (change document settings to generate OpenAPI 3.0).</summary>/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>/// <param name="configure">Configure the document.</param>public static IServiceCollection AddSwaggerDocument(this IServiceCollection serviceCollection, Action<AspNetCoreOpenApiDocumentGeneratorSettings, IServiceProvider> configure = null){serviceCollection.AddSingleton(services =>{var settings = new AspNetCoreOpenApiDocumentGeneratorSettings{SchemaType = SchemaType.Swagger2,};configure?.Invoke(settings, services);...});...}

  个人推荐使用AddOpenApiDocument。  

    services.AddOpenApiDocument(settings =>{//添加代码});

  同样的,无论是AddOpenApiDocument还是AddSwaggerDocument,最终都是依赖AspNetCoreOpenApiDocumentGeneratorSettings来完成,与Swashbuckle不同的是,AddOpenApiDocument方法每次调用只会生成一个swagger接口文档对象,从上面的例子也能看出来:

  DocumentName

  接口文档名,也就是Swashbuckle中SwaggerDoc方法中的name参数。

  Version

  接口文档版本,也就是Swashbuckle中SwaggerDoc方法中的第二个OpenApiInfo的Version属性。

  Title

  接口项目名称,也就是Swashbuckle中SwaggerDoc方法中的第二个OpenApiInfo的Title属性。

  Description

  接口项目介绍,也就是Swashbuckle中SwaggerDoc方法中的第二个OpenApiInfo的Description属性。

  PostProcess

  这个是一个委托,在生成SwaggerDocument之后执行,需要注意的是,因为NSwag有缓存机制的存在PostProcess可能只会执行一遍

  比如:因为NSwag没有直接提供Swashbuckle中SwaggerDoc方法中的第二个OpenApiInfo的Contact属性的配置,这时我们可以使用PostProcess实现。  

    settings.PostProcess = document =>{document.Info.Contact = new OpenApiContact(){Name = "zhangsan",Email = "xxx@qq.com",Url = null};};

  ApiGroupNames

  无论是Swashbuckle还是NSwag都支持生成多个接口文档,但是在接口与文档归属上不一致:

  在Swashbuckle中,通过ApiExplorerSettingsAttribute特性的GroupName属性指定documentName来实现的,而NSwag虽然也是用ApiExplorerSettingsAttribute特性实现,但是此时的GroupName不在是documentName,而是ApiGroupNames属性指定的元素值了:

  比如下面三个接口:  

    /// <summary>/// 未使用ApiExplorerSettings特性,表名属于每一个swagger文档/// </summary>/// <returns>结果</returns>[HttpGet("All"), Authorize]public string All(){return "All";}/// <summary>/// 使用ApiExplorerSettings特性表名该接口属于swagger文档v1/// </summary>/// <returns>Get结果</returns>[HttpGet][ApiExplorerSettings(GroupName = "demo1")]public string Get(){return "Get";}/// <summary>/// 使用ApiExplorerSettings特性表名该接口属于swagger文档v2/// </summary>/// <returns>Post结果</returns>[HttpPost][ApiExplorerSettings(GroupName = "demo2")]public string Post(){return "Post";}

  定义两个文档:  

    services.AddOpenApiDocument(settings =>{settings.DocumentName = "v1";settings.Version = "v0.0.1";settings.Title = "测试接口项目";settings.Description = "接口文档说明";settings.ApiGroupNames = new string[] { "demo1" };settings.PostProcess = document =>{document.Info.Contact = new OpenApiContact(){Name = "zhangsan",Email = "xxx@qq.com",Url = null};};});services.AddOpenApiDocument(settings =>{settings.DocumentName = "v2";settings.Version = "v0.0.2";settings.Title = "测试接口项目v0.0.2";settings.Description = "接口文档说明v0.0.2";settings.ApiGroupNames = new string[] { "demo2" };settings.PostProcess = document =>{document.Info.Contact = new OpenApiContact(){Name = "lisi",Email = "xxx@qq.com",Url = null};};});

  这时不用像Swashbuckle还要在中间件中添加文档地址,NSwag中间件会自动根据路由模板和文档生成文档地址信息,所以直接运行就可以了:

  

  

  可以注意到,All既不属于v1文档也不属于v2文档,也就是说,如果设置了ApiGroupNames,那就回严格的按ApiGroupNames来比较,只有匹配的GroupName在ApiGroupNames属性中才算属于这个接口文档,这也是NSwag和Swashbuckle不同的一点。

  另外,同样的,NSwag也支持使用IActionModelConvention和IControllerModelConvention设置GroupName,具体可以参考上一篇博文。

  UseControllerSummaryAsTagDescription   

  这个属性上面例子有介绍,因为NSwag的控制器标签默认从OpenApiTagAttribute中读取,而不是从注释文档读取,将此属性设置成 true就可以从注释文档读取了,但是读取的内容可被OpenApiTagAttribute特性覆盖。

  AddSecurity

  AddSecurity拓展方法用于添加认证,它是两个重载方法:  

    public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, OpenApiSecurityScheme swaggerSecurityScheme);public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, IEnumerable<string> globalScopeNames, OpenApiSecurityScheme swaggerSecurityScheme);

  虽然是重载,但是两个方法的作用差别还挺大,第一个(不带globalScopeNames参数)的方法的作用类似Swashbuckle的AddSecurityDefinition方法,只是声明的作用,而第二个(有globalScopeNames参数)的方法作用类似于Swashbuckle的AddSecurityRequirement方法,也就是说,这两个重载方法,一个仅仅是声明认证,另一个是除了声明认证,还会将认证全局的作用于每个接口,不过这两个方法的实现是使用DocumentProcessors(类似Swashbuckle的DocumentFilter)来实现的  

    /// <summary>Appends the OAuth2 security scheme and requirement to the document's security definitions.</summary>/// <remarks>Adds a <see cref="SecurityDefinitionAppender"/> document processor with the given arguments.</remarks>/// <param name="settings">The settings.</param>/// <param name="name">The name/key of the security scheme/definition.</param>/// <param name="swaggerSecurityScheme">The Swagger security scheme.</param>public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, OpenApiSecurityScheme swaggerSecurityScheme){settings.DocumentProcessors.Add(new SecurityDefinitionAppender(name, swaggerSecurityScheme));return settings;}/// <summary>Appends the OAuth2 security scheme and requirement to the document's security definitions.</summary>/// <remarks>Adds a <see cref="SecurityDefinitionAppender"/> document processor with the given arguments.</remarks>/// <param name="settings">The settings.</param>/// <param name="name">The name/key of the security scheme/definition.</param>/// <param name="globalScopeNames">The global scope names to add to as security requirement with the scheme name in the document's 'security' property (can be an empty list).</param>/// <param name="swaggerSecurityScheme">The Swagger security scheme.</param>public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, IEnumerable<string> globalScopeNames, OpenApiSecurityScheme swaggerSecurityScheme){settings.DocumentProcessors.Add(new SecurityDefinitionAppender(name, globalScopeNames, swaggerSecurityScheme));return settings;}

  而SecurityDefinitionAppender是一个实现了IDocumentProcessor接口的类,它实现的Porcess如下,其中_scopeNames就是上面方法传进来的globalScopeNames:

    /// <summary>Processes the specified Swagger document.</summary>/// <param name="context"></param>public void Process(DocumentProcessorContext context){context.Document.SecurityDefinitions[_name] = _swaggerSecurityScheme;if (_scopeNames != null){if (context.Document.Security == null){context.Document.Security = new Collection<OpenApiSecurityRequirement>();}context.Document.Security.Add(new OpenApiSecurityRequirement{{ _name, _scopeNames }});}}

  至于其他用法,可以参考上面的一般用法和上一篇中介绍的Swashbuckle的AddSecurityDefinition方法和AddSecurityRequirement方法的用法,很相似。

  DocumentProcessors

  DocumentProcessors类似于Swashbuckle的DocumentFilter方法,只不过DocumentFilter方法时实现IDocumentFilter接口,而DocumentProcessors一个IDocumentProcessor集合属性,是需要实现IDocumentProcessor接口然后添加到集合中去。需要注意的是,因为NSwag有缓存机制的存在DocumentProcessors可能只会执行一遍

  另外,你可能注意到,上面有介绍过一个PostProcess方法,其实个人觉得PostProcess和DocumentProcessors区别不大,但是DocumentProcessors是在PostProcess之前调用执行,源码中:  

    public async Task<OpenApiDocument> GenerateAsync(ApiDescriptionGroupCollection apiDescriptionGroups){...foreach (var processor in Settings.DocumentProcessors){processor.Process(new DocumentProcessorContext(document, controllerTypes, usedControllerTypes, schemaResolver, Settings.SchemaGenerator, Settings));}Settings.PostProcess?.Invoke(document);return document;}

  可能是作者觉得DocumentProcessors有点绕,所以提供了一个委托供我们简单处理吧,用法也可以参考上一篇中的Swashbuckle的DocumentFilter方法,比如全局的添加认证,全局的添加Server等等。

  OperationProcessors

  OperationProcessors类似Swashbuckle的OperationFilter方法,只不过OperationFilter实现的是IOperationFilter,而OperationProcessors是IOperationProcessor接口集合。需要注意的是,因为NSwag有缓存机制的存在OperationProcessors可能只会执行一遍

  同样的,可能作者为了方便我们使用,已经定义好了一个OperationProcessor类,我们可以将我们的逻辑当做参数去实例化OperationProcessor类,然后添加到OperationProcessors集合中即可,不过作者还提供了一个AddOperationFilter方法,可以往OperationProcessors即可开始位置添加过期操作:  

    /// <summary>Inserts a function based operation processor at the beginning of the pipeline to be used to filter operations.</summary>/// <param name="filter">The processor filter.</param>public void AddOperationFilter(Func<OperationProcessorContext, bool> filter){OperationProcessors.Insert(0, new OperationProcessor(filter));}

  所以我们可以这么用:  

    settings.AddOperationFilter(context =>{//我们的逻辑return true;});

  另外,因为无论使用AddOperationFilter方法,还是直接往OperationProcessors集合中添加IOperationProcessor对象,都会对所有Action(或者说Operation)进行调用,NSwag还有一个SwaggerOperationProcessorAttribute特性,用于指定某些特定Action才会调用执行。当然,SwaggerOperationProcessorAttribute的实例化需要指定一个实现了IOperationProcessor接口的类型以及实例化它所需要的的参数。

  与Swashbuckle不同的是,IOperationProcessor的Process接口要求返回一个bool类型的值,表示接口是否要在swaggerUI页面展示,如果返回false,接口就不会在前端展示了,而且后续的IOperationProcessor对象也不再继续调用执行。  

    private bool RunOperationProcessors(OpenApiDocument document, Type controllerType, MethodInfo methodInfo, OpenApiOperationDescription operationDescription, List<OpenApiOperationDescription> allOperations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver){var context = new OperationProcessorContext(document, operationDescription, controllerType,methodInfo, swaggerGenerator, Settings.SchemaGenerator, schemaResolver, Settings, allOperations);// 1. Run from settingsforeach (var operationProcessor in Settings.OperationProcessors){if (operationProcessor.Process(context)== false){return false;}}// 2. Run from class attributesvar operationProcessorAttribute = methodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes()// 3. Run from method attributes.Concat(methodInfo.GetCustomAttributes()).Where(a => a.GetType().IsAssignableToTypeName("SwaggerOperationProcessorAttribute", TypeNameStyle.Name));foreach (dynamic attribute in operationProcessorAttribute){var operationProcessor = ObjectExtensions.HasProperty(attribute, "Parameters") ?(IOperationProcessor)Activator.CreateInstance(attribute.Type, attribute.Parameters) :(IOperationProcessor)Activator.CreateInstance(attribute.Type);if (operationProcessor.Process(context) == false){return false;}}return true;}

  至于其它具体用法,具体用法可以参考上一篇介绍的Swashbuckle的OperationFilter方法,如给特定Operation添加认证,或者对响应接口包装等等。

  其它配置

  AspNetCoreOpenApiDocumentGeneratorSettings继承于OpenApiDocumentGeneratorSettings和JsonSchemaGeneratorSettings还有茫茫多的配置,感兴趣的自己看源码吧,毕竟它和Swashbuckle差不多,一般的需求都能满足了,实现满足不了,可以使用DocumentProcessors和OperationProcessors来实现,就跟Swashbuckle的DocumentFilter和OperationFilter一样。

  但是有些问题可能就不行了,比如虚拟路径问题,Swashbuckle采用在Server上加路径来实现,而因为NSwag没有像Swashbuckle的AddServer方法,想到可以使用上面的PostProcess方法或者使用DocumentProcessors来实现,但是现实是打脸,因为作者的处理方式是,执行PostProcess方法和DocumentProcessors之后,会把OpenAPIDocument上的Servers先清空,然后再加上当前SwaggerUI所在的域名地址,可能作者觉着这样能满足大部分人的需求吧。但是作者还是提供了其他的方式来操作,会在后面的中间件中介绍

  三、添加Swagger中间件(UseOpenApi、UseSwagger和UseSwaggerUi3、UseSwaggerUi)

  UseOpenApi、UseSwagger

  首先UseOpenApi、UseSwagger和Swashbuckle的UseSwagger的作用一样的,主要用于拦截swagger.json请求,从而可以获取返回所需的接口架构信息,不同点在于NSwag的UseOpenApi、UseSwagger具有缓存机制,也就是说,如果第一次获取到了接口文档,会已json格式将文档加入到本地缓存中,下次直接从缓存获取,因为缓存的存在,所以上面介绍的OperationProcessors和DocumentProcessors都不会再执行了。

  另外,UseSwagger是旧版本,已经不推荐使用了,推荐使用UseOpenApi:  

    app.UseOpenApi(settings =>{//中间件设置});

  OpenApiDocumentMiddlewareSettings

  UseOpenApi依赖OpenApiDocumentMiddlewareSettings对象完成配置过程,主要属性有:

  Path

  Path表示拦截请求的格式,也就是拦截swagger.json的路由格式,这个跟Swashbuckle一样,因为需要从路由知道是哪个文档,然后才能去找这个文档的所有接口解析返回,它的默认值是 /swagger/{documentName}/swagger.json。

  同样的,因为这个值关系比较重要,尽可能不要去修改吧。

  DocumentName

  从上面的Path参数的默认值中可以看到,其中有个{documentName}参数,NSwag并没有要求Path中必须有{documentName}参数。

  如果没有这个参数,就必须指定这个属性DocumentName,只是也就是说NSwag只为一个接口文档服务。

  如果有这个参数,NSwag会遍历所有定义的接口文档,然后分别对Path属性替换掉其中中的{documentName}参数,然后分别拦截每个文档获取架构信息的swagger.json请求。

  PostProcess

  服务注入部分有一个PostProcess方法,功能其实类似于DocumentProcessors,就是对接口文档做一个调整,而现在这里又有一个PostProcess方法,它则是根据当前请求来调整接口文档用的。

  比如,上面有介绍,如果在服务注入部分使用PostProcess方法或者DocumentProcessors添加了Server,是没有效果的,这个是因为NSwag在获取到文档之后,有意的清理了文档的Servers属性,然后加上了当前请求的地址:  

    /// <summary>Generates the Swagger specification.</summary>/// <param name="context">The context.</param>/// <returns>The Swagger specification.</returns>protected virtual async Task<OpenApiDocument> GenerateDocumentAsync(HttpContext context){var document = await _documentProvider.GenerateAsync(_documentName);document.Servers.Clear();document.Servers.Add(new OpenApiServer{Url = context.Request.GetServerUrl()});_settings.PostProcess?.Invoke(document, context.Request);return document;}

  注意到上面的源码,在清理之后,还调用了这个PostProcess委托,因此,我们可以将添加Server部分的代码写到这个PostProcess中:  

    app.UseOpenApi(settings =>{settings.PostProcess = (document, request) =>{//清理掉NSwag加上去的document.Servers.Clear();document.Servers.Add(new OpenApiServer() { Url = "http://localhost:90/NSwag", Description = "地址1" });document.Servers.Add(new OpenApiServer() { Url = "http://127.0.0.1:90/NSwag", Description = "地址2" });//192.168.28.213是我本地IPdocument.Servers.Add(new OpenApiServer() { Url = "http://192.168.28.213:90/NSwag", Description = "地址3" });};});

  看来,作者还是很友好的,做了点小动作还提供给我们一个修改的方法。

  CreateDocumentCacheKey

  上面有提到,NSwag的接口文旦有缓存机制,第一次获取之后就会以json格式被缓存,接下就会从缓存中读取,而CreateDocumentCacheKey就是缓存的键值工厂,用于生成缓存键值用的,如果不设置,那么缓存的键值就是string.Empty。

  那可能会问,如果不想用缓存呢,不妨设置CreateDocumentCacheKey成这样:  

    app.UseOpenApi(settings =>{settings.CreateDocumentCacheKey = request => DateTime.Now.ToString();});

  然后你就会发现,过了一段时间之后,你的程序挂了,OutOfMemory!

  所以,好好的用缓存的,从源码中目前没发现有什么办法可以取消缓存,况且使用缓存可以提高响应速度,为何不用?如果实在要屏蔽缓存,那就是改改源码再编译引用吧。

  ExceptionCacheTime

  既然是程序,那就有可能会抛出异常,获取接口文档架构也不例外,而ExceptionCacheTime表示在获取接口文档发生异常后的一段时间内,使用返回这个异常,ExceptionCacheTime默认是TimeSpan.FromSeconds(10)

  UseSwaggerUi3、UseSwaggerUi

  UseSwaggerUi3、UseSwaggerUi的作用和Swashbuckle的UseSwaggerUI作用是一样,主要用于拦截swagger/index.html页面请求,返回页面给前端。

  UseSwaggerUi返回的是基于Swagger2.0的页面,而UseSwaggerUi3返回的是基于Swagger3.0的页面,所以这里推荐使用UseSwaggerUi3  

    app.UseSwaggerUi3(settings =>{//中间件操作});

  SwaggerUi3Settings

  UseSwaggerUi3依赖SwaggerUi3Settings完成配置,SwaggerUi3Settings继承于SwaggerUiSettingsBase和SwaggerSettings,所以属性比较多,这里介绍常用的一些属性:

  EnableTryItOut

  这个属性很简单,就是设置允许你是否可以在SwaggerUI使用Try it out去调用接口

  DocumentTitle

  这是SwaggerUI页面的Title信息,也就是返回的html的head标签下的title标签值,默认是 Swagger UI

  CustomHeadContent

  自定义页面head标签内容,可以使用自定义的脚本和样式等等,作用于Swashbuckle中提到的HeadContent是一样的

  Path

  Path是SwaggerUI的index.html页面的地址,作用与Swashbuckle中提到的RoutePrefix是一样的

  CustomInlineStyles

  自定外部样式,不是链接,就是具体的样式!

  CustomInlineStyles

  自定义的外部样式文件的链接

  CustomJavaScriptPath

  自定义外部JavaScript脚本文件的连接

  DocumentPath

  接口文档获取架构swagger.json的Url模板,NSwag不需要想Swashbuckle调用SwaggerEndpoint添加文档就是因为它会自动根据这个将所有文档按照DocumentPath的格式进行设置,它的默认值是 /swagger/{documentName}/swagger.json。

  同样的,尽可能不要修改这个属性,如果修改了,切记要和上面介绍的OpenApiDocumentMiddlewareSettings的Path属性同步修改。

  SwaggerRoutes

  这是属性包含了接口文档列表,在Swashbuckle中是通过SwaggerEndpoint方法添加的,但是NSwag会自动生成根据DocumentPath属性自动生成。  

    app.UseSwaggerUi3(settings =>{settings.SwaggerRoutes.Add(new NSwag.AspNetCore.SwaggerUi3Route("demo", "/swagger/v1/swagger.json"));});

  需要注意的是,如果自己往SwaggerRoutes中添加接口文档对象,那么NSwag不会自动生成了,比如上面的例子,虽然定义了多个文档,但是我们手动往SwaggerRoutes添加了一个,那SwaggerUI中就只会显示我们自己手动添加的了。

  TransformToExternalPath

  TransformToExternalPath其实是一个路径转化,主要是转换swagger内部的连接,比如获取架构新的的请求 /swagger/v1/swagger.json和获取swaggerUI页面的连接 /swagger,这个很有用,比如上面提到的虚拟路径处理的一个完整的例子: 

   虚拟路径例子

  比如这里我们的虚拟路径是NSwag,使用IIS部署:

  

  项目运行后

  

  

  四、总结

  后面还有东西就不写了,还是那三个注意点:

  主要就是记住三点:

  1、服务注入使用AddOpenApiDocument方法(尽量不要用AddSwaggerDocument),主要就是生成接口相关信息,如认证,接口注释等等,还有几种过滤器帮助我们实现自己的需求

  2、中间件注入有两个:UseOpenApi(尽量不要使用UseSwagger,后续版本将会被移除)和UseSwaggerUi3(尽量不要使用UseSwaggerUi,后续版本将会被移除):

     UseOpenApi负责返回接口架构信息,返回的是json格式的数据

     UseSwaggerUi3负责返回的是页面信息,返回的是html内容

  3、如果涉及到接口生成的,尽可能在AddOpenApiDocument中实现,如果涉及到UI页面的,尽可能在UseSwaggerUi3中实现

swagger自动创建接口文档用法相关推荐

  1. 使用Swagger自动生成接口文档

    安装swagger 链接: link 1.打开vscode,在终端输入: npm install --save @nestjs/swagger swagger-ui-express 应用swagger ...

  2. Swagger - 自动生成接口文档

    Swagger Swagger可以很方便的直接生成项目的接口,便于前后端的分离式开发,并且它还具备调试等功能,可以说十分方便.以这篇文章记录一些Swagger在Springboot项目开发中的使用. ...

  3. DRF 自动生成接口文档

    Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...

  4. idea swagger生成接口文档_Spring Boot(九)Swagger2自动生成接口文档和Mock模拟数据...

    一.简介 在当下这个前后端分离的技术趋势下,前端工程师过度依赖后端工程师的接口和数据,给开发带来了两大问题: 问题一.后端接口查看难:要怎么调用?参数怎么传递?有几个参数?参数都代表什么含义? 问题二 ...

  5. Django框架深入了解_05 (Django中的缓存、Django解决跨域流程(非简单请求,简单请求)、自动生成接口文档)(一)

    阅读目录 一.Django中的缓存: 前戏: Django中的几种缓存方式: Django中的缓存应用: 二.跨域: 跨域知识介绍: CORS请求分类(简单请求和非简单请求) 示例: 三.自动生成接口 ...

  6. Spring Boot(九)Swagger2自动生成接口文档和Mock模拟数据

    一.简介 在当下这个前后端分离的技术趋势下,前端工程师过度依赖后端工程师的接口和数据,给开发带来了两大问题: 问题一.后端接口查看难:要怎么调用?参数怎么传递?有几个参数?参数都代表什么含义? 问题二 ...

  7. SpringBoot自动生成接口文档

    跟大家介绍一个自动生成接口文档的工具包,作者的理念是注释即文档,在写代码的时候写上注释,项目启动后就会生成接口文档,非常方便,省去了Swagger写注解的过程. 仓库地址:https://github ...

  8. 自动生成接口文档之JApiDocs教程

    JApiDocs教程 前言 作为一名优秀的程序员来说,由于涉及到要与前端进行对接,所以避免不了的就是写接口文档.写完接口文档,一旦代码返回结果,参数等出现变动,接口文档还得随之改动,十分麻烦,违背了我 ...

  9. Django框架深入了解_05 (Django中的缓存、Django解决跨域流程(非简单请求,简单请求)、自动生成接口文档)(二)

    二.跨域: 回到顶部 跨域知识介绍: 点我以前博客 跨域解决方法:CORS:跨域资源共享 CORS请求分类(简单请求和非简单请求) 简单请求(simple request):只需要在头信息之中增加一个 ...

最新文章

  1. TYVJ 矩阵取数 Label:高精度+dp
  2. 腾讯推出高性能 RPC 开发框架
  3. assignment makes pointer from integer without a...
  4. 单例模式 之 单例模式——饿汉模式
  5. 如何解决多地开视频会议直播问题?
  6. linux c 实现try catch异常捕获
  7. 单例模式在多线程中的安全性研究
  8. 区分 JSON 字符串与JSON对象
  9. 的正确使用_如何正确使用安全带 安全带正确系法
  10. 【Spark】Spark Stream读取kafka写入kafka报错 AbstractMethodError
  11. hibernate mysql语句_打印hibernate的SQL语句的几种办法
  12. 子类重写方法aop切不到_Spring-aop 全面解析(从应用到原理)
  13. android安卓字体下载,安卓Android简中综艺手机字体
  14. win7计算机高级还原,win7怎么恢复出厂设置 win7恢复出厂设置方法【图文】
  15. 集合问题 : 容斥原理
  16. 如何实现chrome谷歌浏览器多开(独立环境 独立cookie)
  17. 企业向SDN和NFV转型的最大阻碍是CXO们?
  18. 换位密码 java_矩形柱状换位加密方法
  19. 【自然语言处理概述】文本词频分析
  20. Water in Unity 使用Unity提供的水效果

热门文章

  1. aws云平台架构师 收入_收入15万美元的云架构师可能被低薪
  2. gz是什么意思饭圈_光遇:玩家和朋友的日常对话,饭圈女孩太真实了,你有同感吗?...
  3. asp.net+sql 银行营销系统
  4. 05 django路由层
  5. 腾讯应用宝 认领应用 签名空包 流程 最详步骤解析
  6. Exoplayer源码解析2
  7. 腾达fh365虚拟服务器,腾达(Tenda)FH365路由器怎么设置? | 192路由网
  8. 淘宝客订单查询增加了第三方订单查询接口
  9. Python人物头像动漫化
  10. 微信小程序如何发送短信验证码,无需搭建服务器