作为铺垫后续阅读源码的一些帮助,开始想直接开始尝试读源码,但是发现上下文连接不紧密,很突兀,所以还是简单介绍下如何使用。最起码从0-1。然后发现他解决的问题, 其实官方文档是有介绍如何使用的,只是比较浅显,想深入理解和发掘它的一些扩展性,还是需要自己多下功夫的,不过经过总结出的经验来说,只要你想学习某一项技术,但凡认真的看过它的官方文档,你就已经超过了60%
的人了.
1.从问题出发
首先我们需要定义一个服务Http接口
.cs
//实现接口
public calss MoneyAppService : ApplicationService ,ITransientDependency
{
[HttpPost]
Task<List<Money>> GetMoneyAsync()
{
return Task.FromResult(new Money(5000));
}
}
如果另外有一个业务服务X中需要调用这个Http 接口怎么处理,通常的做法:
.c#
//1. 在服务X中注入HttpClient,或者其他的Http请求库
//2. 调用远程接口`GetMoneyAsync`
public Task MockCallAsync()
{
var url = $"{Appsetting.GetUrl(xxxx)}/User/GetUserByOrgIdsList";
var result = await _httpService
.RequestHeader(url, 1000 * 10)
.PostJsonAsync(input)
.ReceiveJson<ResultDto>();
//...
// ....
}
我们思考一下存在哪些问题,如果不知道怎么思考,考虑下如果存在几百个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接口实现定义的接口。
.cs
public interface IMoneyAppService : IRemoteService
{
Task<List<Money>> GetMoneyAsync();
}
public class MoneyAppService : IMoneyAppService
{
[HttpGet]
public Task<List<Money>> GetMoneyAsync()
{
}
}
这里继承自
IRemoteService
的作用在于,框架底层在运行时会通过反射找到继承自它的接口,属于一个标记,当然也可以使用特性。定义接口作用在于对业务接口抽象出一套规范,给另外的服务引用后直接注入接口调用
2.客户端集成调用
在这之前需要把服务端的DLL引用到您本地,直接拷贝或者搭建属于自己的nuget仓库,上传然后下载都可以。
- 引入Abp vNext提供的
Volo.Abp.Http.Client
包。 - 在需要使用的客户端中注入框架提供的模块类和服务端模块类,并在方法中引入动态代理客户端
.cs
//用来创建客户端代理,包含应用服务接口
[DependsOn( typeof(AbpHttpClientModule), typeof(MoneyApplicationContractsModule))]
public class MyClientAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//创建动态客户端代理,这里有几个参数
//1. 服务端接口所在程序集
//2. 服务端的名字,跟配置文件的服务名一致就好了
//3. 是否是默认服务,如果你是远程,就写false,如果是本地,可以不写,因为默认就是true
context.Services.AddHttpClientProxies(
typeof(MoneyApplicationContractsModule).Assembly
,"MoneyService"
, false
);
}
}
3.在配置文件中增加如下节点,其实本质就是配置,客户端需要连接的服务端地址,然后给服务端取一个名字,叫MoneyService。
- 这样做的目的就是框架在内部运行时,对于这个远程服务创建一个具名为
MoneyService
的HttpClient,并把服务地址预先存起来。 - 可以理解为,内部维护一个字典集合,键为
服务名
,值是远程地址
,在调用时根据某些特定标记,它能知道请求哪个服务的地址。
.cs
{
"RemoteServices": {
"MoneyService": {
"BaseUrl": "http://localhost:8080/"
}
}
}
4.在业务代码中注入远程服务的接口
.cs
public class MyService : ITransientDependency
{
private readonly IMoneyAppService _moneyService;
//注入Http代理服务
public MyService(IHttpClientProxy<IMoneyAppService> moneyService)
{
_moneyService = moneyService.Service;
}
public async Task DoIt
{
//像内部代码一样调用远程接口
var moneys= await_moneyService.GetMoneyAsync();
}
}
截止到这里,我想你应该大概理解它的作用以及他的特点,不过我没有按照官方文档上把一些东西复制粘贴过来,只是大概的列出了关键的步骤,然后说明了一些步骤的原因。不是一个教你如何使用的教程,因为官网上写的已经非常好了,假如您只是入门,那么看这个没有意义,如果你压根没有了解过或者使用过ABP的动态代理,为了保持同频,我建议可以还是去看下官方文档,并亲自按照文档来写个demo实验一下,因为后续我分享的并不属于"基础内容",而是去理解它如何实现的。
额,可能还有一个大的疑问,除了ABP提供的,我能有其他方法实现吗? 当然可以
,后面有机会会介绍一些其他比较优秀的库和框架都由此类功能, 例如 refit
,grpc
,dapr
,不过话说回来,知其所以然后, 完全有能力自己实现一套.
3.核心问题与目标
按照上面的介绍,其实觉得最酷的是它可以按照调用内部接口一样调用远程Http接口,而搞清楚他的内部实现原理是我们分享主要的目的.
先提出问题确定我们的主线目标,看源码再从源码中找到解决办法和答案,然后在开发者角度去看为什么这么做,这个过程是很重要的.
在进行下去之前,鉴于源码中使用到了这些技术,所以您必须有如下基础,才能保证你完全理解甚至更透彻的理解:
- 熟练使用 C#反射和泛型以及委托的知识。
- 使用过依赖注入相关知识的功能。
- 对代理模式有一定了解,请说出,装饰器和代理模式的区别...
- 了解AOP是什么,并且使用或者知道
Castle.Core
首先假设如果要我们自己实现一个这样的功能,我们应该如何去思考,并抛出哪些问题?
- 代理实现机制:这个功能最大的特点就是,业务写在另外的程序,需要有人帮我发送Http请求,谁来发? 怎么发送呢?
- 服务映射关系: 通常在微服务情况下是多个远程服务,怎么保证不会在我使用的时候请求错目标,并且怎么映射远程服务和具体http 接口的关系?
- HTTP方法选择:请求远程服务的方法,是Post还是 Get?
- 参数与返回值处理:如何将方法参数转换为HTTP请求体或查询参数?
4. 阅读源码
再次明确一下我们是带着问题来找答案的,不是盲目看,再回顾一次我们的问题
1.代理机制
1.直接找到注入服务的位置,Netcore的固定套路,从配置或者中间件中找答案


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

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

描述如下
3.最终根据内容发起远程调用返回结果
调用时序

总结
在读完之后会发现这里的做法真的很巧妙,很简洁,在很多业务代码中是看不到的,这小小的一段代码,用到了很多知识.
泛型、反射、依赖注入、委托、
代理模式、适配器、Castle.Core技术
当然在使用过程中发现了有一些不足
- 例如探测元数据时,如果被调用服务停机更新,在上线后,调用方必须重启探测才能更新描述,不然不能请求到新的方法,因为内部使用的是程序缓存,如果再分布式下,可以改写为redis缓存
- 例如如何针对一个请求设置超时设置,这个在新的里面已经实现了
这次分享的核心的就是如何靠动态代理实现代理并实际应用,当然abp也有自带的静态代理实现,后续分享一下Refit库,它的内部就是通过Roslyn在编译时进行静态代理.