找回密码
 立即注册
首页 业界区 业界 Abp vNext -动态 C# API 实现原理解析

Abp vNext -动态 C# API 实现原理解析

镝赋洧 昨天 17:12
作为铺垫后续阅读源码的一些帮助,开始想直接开始尝试读源码,但是发现上下文连接不紧密,很突兀,所以还是简单介绍下如何使用。最起码从0-1。然后发现他解决的问题, 其实官方文档是有介绍如何使用的,只是比较浅显,想深入理解和发掘它的一些扩展性,还是需要自己多下功夫的,不过经过总结出的经验来说,只要你想学习某一项技术,但凡认真的看过它的官方文档,你就已经超过了60%的人了.
1.从问题出发

首先我们需要定义一个服务Http接口
  1. //实现接口
  2. public calss MoneyAppService : ApplicationService ,ITransientDependency
  3. {
  4.    [HttpPost]
  5.    Task<List<Money>> GetMoneyAsync()
  6.    {
  7.       return Task.FromResult(new Money(5000));
  8.    }
  9. }
复制代码
如果另外有一个业务服务X中需要调用这个Http 接口怎么处理,通常的做法:
  1. //1. 在服务X中注入HttpClient,或者其他的Http请求库
  2. //2. 调用远程接口`GetMoneyAsync`
  3. public Task MockCallAsync()
  4. {
  5.     var url = $"{Appsetting.GetUrl(xxxx)}/User/GetUserByOrgIdsList";
  6.     var result = await _httpService
  7.      .RequestHeader(url, 1000 * 10)
  8.      .PostJsonAsync(input)
  9.      .ReceiveJson<ResultDto>();
  10.     //...
  11.     // ....
  12. }
复制代码
我们思考一下存在哪些问题,如果不知道怎么思考,考虑下如果存在几百个http接口,怎么办?
1.当远程接口达到一定数量,需要代码调用远程接口的地方,充满了很多HttpClient,所以结论是不优雅的,没有抽象。
2.到处都是字符串url和弱类型的参数,对请求和响应要做一堆的校验
3.虽然是不同的服务,但是需要相互调用,一定是业务有衔接, 所以理论上彼此之间发生调用的业务应该是形成规范的,包括接口的方法名,出参、入参 都不应该随意定义,那如何形成规范呢?
2.动态HttpClient

ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS).通过这种方式,你不需要通过 HttpClient 或者其他低级的HTTP功能调用远程服务并获取数据.
这段话是Abp官方在手册上写的一段话,翻译成大白话的意思就是:

  • 使用 ABP提供的动态C# API 客户端之后,你访问远程服务,就不需要在代码中直接注入HttpClient,或者下载安装其他的一些什么Http请求的库.
  • 只要你集成它,你就可以像调用本地代码一样,来调用远程服务,还不明白? 看例子!!!
  • 支持接口约束,某种意义保护了你的业务完整性.
按照上面描述的,我们按照官网的关键步骤来试一下
1.声明一个C# 接口,定义一个方法,并将Http接口实现定义的接口。
  1. public interface IMoneyAppService : IRemoteService
  2. {
  3.     Task<List<Money>> GetMoneyAsync();
  4. }
  5. public class MoneyAppService : IMoneyAppService
  6. {
  7.     [HttpGet]
  8.     public  Task<List<Money>> GetMoneyAsync()
  9.     {
  10.     }
  11. }
复制代码
这里继承自IRemoteService的作用在于,框架底层在运行时会通过反射找到继承自它的接口,属于一个标记,当然也可以使用特性。
定义接口作用在于对业务接口抽象出一套规范,给另外的服务引用后直接注入接口调用
2.客户端集成调用
在这之前需要把服务端的DLL引用到您本地,直接拷贝或者搭建属于自己的nuget仓库,上传然后下载都可以。

  • 引入Abp vNext提供的 Volo.Abp.Http.Client包。
  • 在需要使用的客户端中注入框架提供的模块类和服务端模块类,并在方法中引入动态代理客户端
  1. //用来创建客户端代理,包含应用服务接口
  2. [DependsOn( typeof(AbpHttpClientModule), typeof(MoneyApplicationContractsModule))]
  3. public class MyClientAppModule : AbpModule
  4. {
  5.     public override void ConfigureServices(ServiceConfigurationContext context)
  6.     {
  7.         //创建动态客户端代理,这里有几个参数
  8.         //1. 服务端接口所在程序集
  9.         //2. 服务端的名字,跟配置文件的服务名一致就好了
  10.         //3. 是否是默认服务,如果你是远程,就写false,如果是本地,可以不写,因为默认就是true
  11.         context.Services.AddHttpClientProxies(
  12.             typeof(MoneyApplicationContractsModule).Assembly
  13.            ,"MoneyService"
  14.            , false
  15.         );
  16.     }
  17. }
复制代码
3.在配置文件中增加如下节点,其实本质就是配置,客户端需要连接的服务端地址,然后给服务端取一个名字,叫MoneyService。

  • 这样做的目的就是框架在内部运行时,对于这个远程服务创建一个具名为MoneyService 的HttpClient,并把服务地址预先存起来。
  • 可以理解为,内部维护一个字典集合,键为服务名,值是远程地址,在调用时根据某些特定标记,它能知道请求哪个服务的地址。
  1. {
  2.     "RemoteServices": {
  3.         "MoneyService": {
  4.             "BaseUrl": "http://localhost:8080/"
  5.         }
  6.     }
  7. }
复制代码
4.在业务代码中注入远程服务的接口
  1. public class MyService : ITransientDependency
  2. {
  3.     private readonly IMoneyAppService _moneyService;
  4.     //注入Http代理服务
  5.     public MyService(IHttpClientProxy<IMoneyAppService> moneyService)
  6.     {
  7.         _moneyService = moneyService.Service;
  8.     }
  9.     public async Task DoIt
  10.     {
  11.         //像内部代码一样调用远程接口
  12.         var moneys= await_moneyService.GetMoneyAsync();
  13.     }
  14. }
复制代码
截止到这里,我想你应该大概理解它的作用以及他的特点,不过我没有按照官方文档上把一些东西复制粘贴过来,只是大概的列出了关键的步骤,然后说明了一些步骤的原因。不是一个教你如何使用的教程,因为官网上写的已经非常好了,假如您只是入门,那么看这个没有意义,如果你压根没有了解过或者使用过ABP的动态代理,为了保持同频,我建议可以还是去看下官方文档,并亲自按照文档来写个demo实验一下,因为后续我分享的并不属于"基础内容",而是去理解它如何实现的。
额,可能还有一个大的疑问,除了ABP提供的,我能有其他方法实现吗? 当然可以 ,后面有机会会介绍一些其他比较优秀的库和框架都由此类功能, 例如 refit ,grpc,dapr,不过话说回来,知其所以然后, 完全有能力自己实现一套.


3.核心问题与目标

按照上面的介绍,其实觉得最酷的是它可以按照调用内部接口一样调用远程Http接口,而搞清楚他的内部实现原理是我们分享主要的目的.
先提出问题确定我们的主线目标,看源码再从源码中找到解决办法和答案,然后在开发者角度去看为什么这么做,这个过程是很重要的.
在进行下去之前,鉴于源码中使用到了这些技术,所以您必须有如下基础,才能保证你完全理解甚至更透彻的理解:

  • 熟练使用 C#反射和泛型以及委托的知识。
  • 使用过依赖注入相关知识的功能。
  • 对代理模式有一定了解,请说出,装饰器和代理模式的区别...
  • 了解AOP是什么,并且使用或者知道 Castle.Core
首先假设如果要我们自己实现一个这样的功能,我们应该如何去思考,并抛出哪些问题?

  • 代理实现机制:这个功能最大的特点就是,业务写在另外的程序,需要有人帮我发送Http请求,谁来发? 怎么发送呢?
  • 服务映射关系: 通常在微服务情况下是多个远程服务,怎么保证不会在我使用的时候请求错目标,并且怎么映射远程服务和具体http 接口的关系?
  • HTTP方法选择:请求远程服务的方法,是Post还是 Get?
  • 参数与返回值处理:如何将方法参数转换为HTTP请求体或查询参数?
4. 阅读源码

再次明确一下我们是带着问题来找答案的,不是盲目看,再回顾一次我们的问题
1.代理机制

1.直接找到注入服务的位置,Netcore的固定套路,从配置或者中间件中找答案
1.png

2.png

2.根据筛选规则后的程序集注册
3.png

其实此时对于熟悉的伙伴就知道,Castle.Core的影子出现了,必定跟动态代理有关

  • 先将给定程序集中的一些类进行反射,在IsSuitableForClientProxying方法中,排除掉不合适的类型,找到适合被代理的类型 [公共不是泛型并继承自IRemoteService] 的接口类型.
  • 然后将找到的类型进行循环的注册添加客户端代理,其实这里按照demo中注册的类型,最终在serviceTypes里就是 IMoneyAppService.
  • 在第二部分代码中,最主要的事情就是,将业务接口通过IHttpClientProxy封装, 内部使用 CreateInterfaceProxyWithoutTarget方法创建了一个没有目标实现的接口代理。这说明当调用接口方法时,不会有一个实际的对象去执行方法,而是由拦截器(Interceptor)来处理。这里使用了两个拦截器:验证拦截器(validationInterceptorAdapterType)和适配器拦截器(interceptorType 用于代理HTTP调用),最终这个委托返回的是一个HttpClientProxy.
  • 注册完成后,服务中使用IHttpClientProxy来引入对象,而对象的内部Service是具体的动态代理对象.
  • 当程序使用具体的方法调用时,会被代理拦截,先执行参数校验拦截器,然后进行 HttpClient 拦截器调用.
代理注册完整流程
通俗点意思就是,我引用了你的接口,直接调用肯定没有实现,这时候我伪造一个你的接口实现出来,然后通过这个冒牌的内部调用远程Http接口
4.png

2. 服务调用

原本是业务代码该发送请求的调用的,现在需要有人帮我做这件事,甚至我有可能会在做这件事的前后进行一些额外的事情,其实有经验的小伙伴就知道,可以使用AOP,那设计者是怎么设计并且兼顾这2点的呢?那就是使用代理模式,此处的代理模式不是静态代理,因为静态代理是写入实际的代码,这里使用了Castle.Core来进行动态代理,也就是说可以更加灵活,,只是这里设计者包装了一层ABP风格的拦截器,最后使用适配器模式,将Abp风格的拦截器,适配进Castle.Core自带的拦截器,最终运行时,发起请求后,还是被Castle.Core的拦截器进行拦截,接收到请求,转发给ABP自己定义规范的拦截器,在这里最终就是在上面注册的第一个DynamicHttpProxyInterceptor拦截器,并且由他来作为代理,发起请求,如果不明白意思的小伙伴,一定要去把Castle.Core体验一下,当然前提是你对AOP和代理模式有一定认知,否则看的云里雾里.**
上面聊到已经注册完成我再调用时直接使用本地方法一样来调用
  1.     public async Task DoIt
  2.     {
  3.         //像内部代码一样调用远程接口
  4.         var moneys= await_moneyService.GetMoneyAsync();
  5.     }
复制代码
这个调用过程会经历2个拦截器 参数校验拦截器 和 动态Http代理拦截器,咱们这次重点关注,它是如何帮我发送请求的部分
1.直接进入冒牌的实现(不太恰当,这么理解也没错) DynamicHttpProxyInterceptor类的 InterceptAsync方法,不知道为什么的,得补补基础知识
5.png

这里分为2步

  • 第一步先讲被调用的方法的元数据信息经过处理包装到一个ClientProxyRequestContext上下文
  • 第二步就是判断被调用方法的返回类型,如果没有泛型参数就直接调用并等待返回,如果有提取泛型参数类型,调用然后提取转换为结果
3. 远程HTTP方法匹配

1.在组装ClientProxyRequestContext上下文时,调用了一个GetActionApiDescriptionModel方法
6.png

2.内部调用了一个探测请求,拿到所有的服务描述,然后根据描述中的内容匹配上下文的方法和参数,找到具体的远程处理方法
访问(目标服务/api/abp/api-definition)就能拿到json格式的描述元数据
7.png

描述如下
8.png

3.最终根据内容发起远程调用返回结果
调用时序
9.png

总结

在读完之后会发现这里的做法真的很巧妙,很简洁,在很多业务代码中是看不到的,这小小的一段代码,用到了很多知识.

  • 泛型、反射、依赖注入、委托、
  • 代理模式、适配器、Castle.Core技术
当然在使用过程中发现了有一些不足

  • 例如探测元数据时,如果被调用服务停机更新,在上线后,调用方必须重启探测才能更新描述,不然不能请求到新的方法,因为内部使用的是程序缓存,如果再分布式下,可以改写为redis缓存
  • 例如如何针对一个请求设置超时设置,这个在新的里面已经实现了
这次分享的核心的就是如何靠动态代理实现代理并实际应用,当然abp也有自带的静态代理实现,后续分享一下Refit库,它的内部就是通过Roslyn在编译时进行静态代理.

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册