1、前言
NCC WebApiClient 已成熟稳定,发布了WebApiClient.JIT 和 WebApiClient.AOT 两个 NuGet 包,累计近 10w 次下载。
我对它的高可扩展性设计相当满意和自豪,但 WebApiClient 并不因此而停下脚步,在一年前,我产生了编写其 Core 版本的想法,将 ASP.NET Core 服务端先进的思想融入到 Core 版本,在性能与扩展性上得到进一步升华。
对应的,给它叫了 WebApiClientCore
的名字,为了对得起名字里面的 Core 字,我在框架设计、性能优化上占用整体开发时间一半以上。
2、框架设计
IActionInvoker
WebApiClient 时还没有 IActionInvoker
概念,对应的执行逻辑直接在 ApiActionContext
上实现。现在我觉得,Context 应该是一个状态数据类,而不能也成为一个执行者,因为一个执行者的实例可以无限次地执行多个 Context 实例。
Refit 则更简单粗暴,将所有实现都在一个 RequestBuilderImplementation
的类上:你们只要也只能使用我内置的 Attribute 声明,一切执行在我这个类里面包办,因为我是一个万能类。
Core 版本增加了 IActionInvoker
概念,从中 Context 分开,用于执行 Context,职责分明。在实现上又分为多种 Invoker:Task
声明返回执行者 ActionInvoker
、ITask
声明返回处理处理者 ActionTask
,以及聚合的 MultiplexedActionInvoker
。
Middleware思想
WebApiClient 时在处理各个特性、参数验证、返回值验证时没有使用 Middleware 思想,特别是在处理响应结果和异常短路逻辑难以维护。
Refit 还是简单粗暴,将所有特性的解释实现都在这个 RequestBuilderImplementation
的类上,因为我还是一个万能类。
Core 版本增加中间件 Builder,将请求前的相关 Attribute 的执行编排 Build 为一个请求处理委托,将请求后相关 Attribute 的执行编排 Build 为一个响应处理委托,然后把两个委托与真实 http 请求串在一起,Build 出一个完整的请求响应委托。
得益于 Middleware,流程中的请求前参数值验证、结果处理特性短路、异常短路、请求后结果值验和无条件执行 IApiFilterAtrribue
等这些复杂的流程变成简单的管道处理;另外接口也变成支持服务端响应多种格式内容,每种格式内容在一个 IApiReturnAttribute
上实现和处理,比如请求为 Accept: application/json, application/xml
,不管服务器返回xml或json都能处理。
/// <summary>
/// 创建执行委托
/// </summary>
/// <param name="apiAction">action描述器</param>
/// <returns></returns>
public static Func<ApiRequestContext, Task<ApiResponseContext>> Build(ApiActionDescriptor apiAction)
{var requestHandler = BuildRequestHandler(apiAction);var responseHandler = BuildResponseHandler(apiAction);return async request =>{await requestHandler(request).ConfigureAwait(false);var response = await HttpRequest.SendAsync(request).ConfigureAwait(false);await responseHandler(response).ConfigureAwait(false);return response;};
}
Context 思想
WebApiClient 只有一个 ApiActionContext,其 Result 和 Exception 属性在请求前就可以访问或设置,但实际上就算设置了值,流程也不会短路和中断,属于设计失误。
Refit 没有相关 Context 概念,因为它不提供给用户自定义扩展 Attribute 的能力,它内置的 Attribute 也没有执行能力,一个 RequestBuilderImplementation
类够了。
Core 版本将设计了多个 Context 概念,不同阶段有不同的 Context,如同 ASP.NET Core 不同 Filter 的 Context 也不同一样。对于一个 Action,请求阶段对应是 ApiRequestContext
,响应阶段是 ApiResponseContext
;对于 Action 的参数,对应是 ApiParameterContext
。每种 Context 里面都包含核心的 HttpContext
属性,HttpContext
包含请求消息、响应消息和接口配置选项等。
Interface 思想
输入 WebApiClientCore 命名空间,会发现定义了很多 Interface,这些 Interface 都是为了用户实现自定义特性用的,当然内置的所有特性,都是实现了这些接口而已。如果一个特性实现了多个接口,它就有多种能力,比如内置的 HeaderAttribute
,它既可以修饰于 Interface 和 Method,也可以修饰参数。
WebApiClientCore 的 Attribute 描述的逻辑,是由 Attribute 自我实现,所以整个请求的数据装配逻辑是分散为各个 Attribute 上,用什么 Attribute 就有什么逻辑,包含框架之外的非内置的自定义 Attribute。
Refit 的内置 Attribute 只有欲描述逻辑,没有实现逻辑,实现逻辑由 RequestBuilderImplementation
包办,所以它不需要接口也没有接口。
3、性能优化
更快的字符串替换
像[HttpGet("objects/{id}")]这样的path参数,在 RESTFul 中常常遇到,通过Span
优化,Core 版本在替换 path
参数值 CPU 占用时间降低为原版本的十分之一。
更快的 JSON 序列化
得益于 Sytem.Text.Json
,JSON 序列化和反序列化上性能显明提升。
更少的缓冲区分配
WebApiClientCore 使用了可回收复用的 IBufferWriter
,在 JSON 序列化得到 Json、Json 装配为 HttpContent
只申请一次 Buffer,而且 HttpContent
在发送之后,这个 Buffer 被回收复用。IBufferWriter
还于用表单的 URI 编码,编码产生的 Buffer 不用申请新的内存内容,直接写入表单的 HttpContent
。
更少的编码操作
WebApiClientCore 的 JSON 不再使用 UTF16 的 string 中间类型,直接将对象序列化为网络请求需要的 UTF8 编码二进制 JSON;表单的 Key 和 Value 编码时,也不产生 string 中间类型,而是编码后的二进制数据内容,然后写入表单的 IBufferWriter
。
更快的缓存查找
WebApiClient 创建代理类实例来执行一个请求时,要查找两次缓存:通过接口类型查找字典缓存的接口代理类,然后实例化代理类;在 ApiInterceptor
里面通过 MethodInfo
查找字典缓存的 ApiActionDescriptor
。
Refit 执行同样逻辑也使用了两次字典缓存,接口和接口代理类安全字典缓存 TypeMapping
,接口和接口方法描述的字典缓存 InterfaceHttpMethods
。
WebApiClientCore 取消了字典缓存,使用静态泛型类的字段作缓存,因为字段访问远比字典查找高效。同时通过巧妙的设计,在代理类拦截方法执行时,直接回传 IActionInvoker
替换原来的 MethodInfo
,IActionInvoker
包含了ApiActionDescriptor
,而 IActionInvoker
与代理类型都一起缓存在静态泛型类的字段,减少了一次必须的字典缓存查找过程。
性能对比
排除掉真实的网络请求IO等待时间,WebApiClientCore 使用的 CPU 时间仅仅为 WebApiClient.JIT 和 Refit 的三分之一。
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.836 (1903/May2019Update/19H1)
Intel Core i3-4150 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=3.1.202[Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJITDefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
Method | Mean | Error | Std Dev |
---|---|---|---|
HttpClient_ GetAsync | 3.146 μs | 0.0396 μs | 0.0370 μs |
WebApiClientCore_ GetAsync | 12.421 μs | 0.2324 μs | 0.2174 μs |
Refit_ GetAsync | 43.241 μs | 0.6713 μs | 0.6279 μs |
Method | Mean | Error | Std Dev |
---|---|---|---|
HttpClient_ PostJsonAsync | 5.263 μs | 0.0784 μs | 0.0733 μs |
WebApiClientCore_ PostJsonAsync | 13.823 μs | 0.1874 μs | 0.1753 μs |
Refit_ PostJsonAsync | 45.218 μs | 0.8166 μs | 0.7639 μs |
4、NuGet 包与文档
NuGet 包
<PackageReference Include="WebApiClientCore" Version="1.0.0" />
项目地址与文档
点击项目链接,带你 GET 到 N 种使用技能,不求 star,只求提供良好建议。
https://github.com/dotnetcore/WebApiClient