Abp Vnext 动态(静态)API客户端源码解析

这篇具有很好参考价值的文章主要介绍了Abp Vnext 动态(静态)API客户端源码解析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

根据以往的经验,通过接口远程调用服务的原理大致如下:
  1. 服务端:根据接口定义方法的签名生成路由,并暴露Api。
  2. 客户端:根据接口定义方法的签名生成请求,通过HTTPClient调用。
这种经验可以用来理解ABP VNext自动API的方式,但如果不使用自动API并且控制器定义了路由的情况下,远程调用的路由地址就有可能跟服务端暴露的路由不一致,预料的结果应该会返回404,但是Abp vnext却能够正常工作。那么客户端在使用远程调用时,是如何知道实际调用方法的路由地址呢?下面我们来探究一下源码。
 

一.动态API客户端

下面是注册动态API客户端的源码,AddHttpClientProxies 方法传入两个参数:接口层程序集和远程服务名称。该方法主要是遍历所有继承 IRemoteService 接口的类型,并为它们注册动态代理。同时,将每个类型的实例与远程服务名称关联起来,以便在进行远程调用时能够根据类型获取到对应的远程配置。需要注意的是,如果配置不存在对应的远程服务名称,则采用默认配置。
 
context.Services.AddHttpClientProxies(
            typeof(IdentityApplicationContractsModule).Assembly,  //接口层程序集
            RemoteServiceName   //远程服务名称
        );
        
public static IServiceCollection AddHttpClientProxy(this IServiceCollection services, Type type, string remoteServiceConfigurationName = "Default", bool asDefaultService = true)
        {
            /*省略一些代码...*/
            Type type2 = typeof(DynamicHttpProxyInterceptor<>).MakeGenericType(type); //拦截器
            services.AddTransient(type2);
            Type interceptorAdapterType = typeof(AbpAsyncDeterminationInterceptor<>).MakeGenericType(type2);
            Type validationInterceptorAdapterType = typeof(AbpAsyncDeterminationInterceptor<>).MakeGenericType(typeof(ValidationInterceptor));
            if (asDefaultService)
            {
                //生成代理,依赖注入到容器
                services.AddTransient(type, (IServiceProvider serviceProvider) => ProxyGeneratorInstance.CreateInterfaceProxyWithoutTarget(type, (IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType), (IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType)));
            }
                         
            services.AddTransient(typeof(IHttpClientProxy<>).MakeGenericType(type), delegate (IServiceProvider serviceProvider)
            {
                //生成代理,通过HttpClientProxy封装,依赖注入到容器
                object obj = ProxyGeneratorInstance.CreateInterfaceProxyWithoutTarget(type, (IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType), (IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType));
                return Activator.CreateInstance(typeof(HttpClientProxy<>).MakeGenericType(type), obj);
            });
            return services;
        }

 

通过动态代理实例调用方法的时候,会先进入拦截器 DynamicHttpProxyInterceptor InterceptAsync 方法。
 
 public override async Task InterceptAsync(IAbpMethodInvocation invocation)
    {
        var context = new ClientProxyRequestContext(     
            await GetActionApiDescriptionModel(invocation), //获取Api描述信息
            invocation.ArgumentsDictionary,
            typeof(TService));

        if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty())
        {
            await InterceptorClientProxy.CallRequestAsync(context);  
        }
        else
        {
            var returnType = invocation.Method.ReturnType.GenericTypeArguments[0];
            var result = (Task)CallRequestAsyncMethod
                .MakeGenericMethod(returnType)
                .Invoke(this, new object[] { context });

            invocation.ReturnValue = await GetResultAsync(result, returnType);  //调用CallRequestAsync泛型方法
        }
    }
 
先通过 GetActionApiDescriptionModel 方法获取到Api描述信息,将其封装进远程调用的上下文。接着调用 CallRequestAsync 方法真正进行远程请求。如果是泛型,则调用 CallRequestAsync 的泛型方法。让我们先来看看 GetActionApiDescriptionModel 方法是如何获取到Api描述信息的。
 
    protected virtual async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel(IAbpMethodInvocation invocation)
    {
        var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ??      //获取远程服务名称
                           throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}.");
        var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName);//获取远程服务端点配置
        var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); //创建HttpClient

        return await ApiDescriptionFinder.FindActionAsync(   
            client,
            remoteServiceConfig.BaseUrl,  //远程服务地址
            typeof(TService),       
            invocation.Method
        );
    }
 
远程服务端点配置例如:
 
 "RemoteServices": {
    "Default": {
      "BaseUrl": "http://localhost:44388"
    },
   "XXXDemo":{
     "BaseUrl": "http://localhost:44345"
     }

  },
 
根据接口类型获取到远程服务名称,再根据名称获取到服务端点配置。ApiDescriptionFinder IApiDescriptionFinder 的实例,默认实现是 ApiDescriptionFinder
 
public async Task<ActionApiDescriptionModel> FindActionAsync(
        HttpClient client,
        string baseUrl,
        Type serviceType,
        MethodInfo method)
    {
        var apiDescription = await GetApiDescriptionAsync(client, baseUrl); //获取Api描述信息并缓存结果

        //TODO: Cache finding?

        var methodParameters = method.GetParameters().ToArray();

        foreach (var module in apiDescription.Modules.Values)
        {
            foreach (var controller in module.Controllers.Values)
            {
                if (!controller.Implements(serviceType))  //不继承接口跳过,所以写控制器为什么需要要继承服务接口的作用之一便在于此
                {
                    continue;
                }

                foreach (var action in controller.Actions.Values)
                {
                    if (action.Name == method.Name && action.ParametersOnMethod.Count == methodParameters.Length) //签名是否匹配
                    {
                        /*省略部分代码 */
                    }
                }
            }
        }

        throw new AbpException($"Could not found remote action for method: {method} on the URL: {baseUrl}");
    }

    public virtual async Task<ApplicationApiDescriptionModel> GetApiDescriptionAsync(HttpClient client, string baseUrl)
    {
        return await Cache.GetAsync(baseUrl, () => GetApiDescriptionFromServerAsync(client, baseUrl)); //缓存结果
    }

 

   protected virtual async Task<ApplicationApiDescriptionModel> GetApiDescriptionFromServerAsync(
        HttpClient client,
        string baseUrl)
    {
        //构造请求信息
        var requestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            baseUrl.EnsureEndsWith('/') + "api/abp/api-definition"
        );

        AddHeaders(requestMessage); //添加请求头

        var response = await client.SendAsync(   //发送请求并获取响应结果
            requestMessage,
            CancellationTokenProvider.Token
        );

        if (!response.IsSuccessStatusCode)
        {
            throw new AbpException("Remote service returns error! StatusCode = " + response.StatusCode);
        }

        var content = await response.Content.ReadAsStringAsync();

        var result = JsonSerializer.Deserialize<ApplicationApiDescriptionModel>(content, DeserializeOptions);

        return result;
    }
 
 
GetApiDescriptionAsync 方法包装了缓存,GetApiDescriptionFromServerAsync 才是真正去获取Api描述信息的方法,它传递了两个参数,一个是httpclient(作用无需多说),另一个是baseurl即远程服务端点地址。通过Get请求方式调用远程服务的 "api/abp/api-definition" 接口,获取到该服务所有API描述信息,然后根据远程调用服务类型跟方法签名找到对应的API描述信息。API描述信息包含了端点的实际路由,支持版本号,是否允许匿名访问等信息。到此API描述信息已经获取到,回过头来看看 CallRequestAsync 方法的实现。
 
public virtual async Task<T> CallRequestAsync<T>(ClientProxyRequestContext requestContext)
    {
        return await base.RequestAsync<T>(requestContext);
    }

    public virtual async Task<HttpContent> CallRequestAsync(ClientProxyRequestContext requestContext)
    {
        return await base.RequestAsync(requestContext);
    }

 

  protected virtual async Task<HttpContent> RequestAsync(ClientProxyRequestContext requestContext)
    {
        //获取远程服务名称
        var clientConfig = ClientOptions.Value.HttpClientProxies.GetOrDefault(requestContext.ServiceType) ?? throw new AbpException($"Could not get HttpClientProxyConfig for {requestContext.ServiceType.FullName}.");
        
        //获取远程服务端点配置
        var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName);

        var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); 
        var apiVersion = await GetApiVersionInfoAsync(requestContext); //获取API版本
        var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + await GetUrlWithParametersAsync(requestContext, apiVersion);  //拼接完整的url
        var requestMessage = new HttpRequestMessage(requestContext.Action.GetHttpMethod(), url) //构造HTTP请求信息
        {
            Content = await ClientProxyRequestPayloadBuilder.BuildContentAsync(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion)
        };

        AddHeaders(requestContext.Arguments, requestContext.Action, requestMessage, apiVersion); //添加请求头

        if (requestContext.Action.AllowAnonymous != true) //是否需要认证
        {
            await ClientAuthenticator.Authenticate(       //认证
                new RemoteServiceHttpClientAuthenticateContext(
                    client,
                    requestMessage,
                    remoteServiceConfig,
                    clientConfig.RemoteServiceName
                )
            );
        }

        HttpResponseMessage response;
        try
        {
            response = await client.SendAsync(   //发送请求
                requestMessage,
                HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/,
                GetCancellationToken(requestContext.Arguments)
            );
        }
        return response.Content;
    }
 
GetUrlWithParametersAsync 方法是根据API描述信息跟调用参数值拼接出完整的路由地址,比如user/{id}/?name=xxxxx,接着构造出HTTP请求信息,添加请求头,如果需要身份认证,则调用 ClientAuthenticator.Authenticate 方法,ClientAuthenticator IRemoteServiceHttpClientAuthenticator 的实例,它的实现有多种,有【Volo.Abp.Http.Client.IdentityModel】模块的 IdentityModelRemoteServiceHttpClientAuthenticator 类,它是使用OAuth 2.0协议直接调用接口获取访问令牌。 有 【Volo.Abp.Http.Client.IdentityModel.Web】 模块的 HttpContextIdentityModelRemoteServiceHttpClientAuthenticator 类,它是从当前请求上下文获取到当前登录用户的访问令牌。
 
 public override async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
    {
        if (context.RemoteService.GetUseCurrentAccessToken() != false)
        {
            var accessToken = await GetAccessTokenFromHttpContextOrNullAsync(); //获取当前登录用户Token
            if (accessToken != null)
            {
                context.Request.SetBearerToken(accessToken);
                return;
            }
        }

        await base.Authenticate(context);
    }
 
如果远程调用需要传递当前登录用户令牌则可以引用 【Volo.Abp.Http.Client.IdentityModel.Web】模块
[DependsOn(typeof(AbpHttpClientIdentityModelWebModule))]
 
端点配置例如:
  "RemoteServices": {
    "AbpMvcClient": {
      "BaseUrl": "http://localhost:44388",
      "UseCurrentAccessToken": "true"
    }
  }
 
AddHeaders 方法,请求头添加租户等信息
 
 protected virtual void AddHeaders(
        IReadOnlyDictionary<string, object> argumentsDictionary,
        ActionApiDescriptionModel action,
        HttpRequestMessage requestMessage,
        ApiVersionInfo apiVersion)
    {
        /*省略代码/*
        //TenantId
        if (CurrentTenant.Id.HasValue)
        {
            //TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
            requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
        }
        /*省略代码/*
    }

 

要点

1.控制器要继承服务接口
2.如果采用内部网关, api/abp/api-definition 将会转发到某一个服务,所以就需要将所有微服务的Api层引用到该服务上(或者在内部网关),这样才能通过 api/abp/api-definition 接口获取到对应服务的API描述信息。否则就不能直接通过内部网关调用,需要配置不同的远程服务名称指向相应的服务上才能获取到API描述信息。
Abp Vnext 动态(静态)API客户端源码解析

 

二.静态API客户端

 
静态API客户端跟动态API客户端不一样,静态API客户端是通过abp cli工具提前生成好调用类跟API描述文件,在生成的时候同样遵守动态API客户端获取API描述信息的规则(注意要点1,2)
生成后ClientProxies目录包含调用类跟 *generate-proxy.json 文件,*generate-proxy.json 文件包含了API描述信息。
生成的调用类如下:
 
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityRoleAppService), typeof(IdentityRoleClientProxy))]
public partial class IdentityRoleClientProxy : ClientProxyBase<IIdentityRoleAppService>, IIdentityRoleAppService
{
    public virtual async Task<ListResultDto<IdentityRoleDto>> GetAllListAsync()
    {
        return await RequestAsync<ListResultDto<IdentityRoleDto>>(nameof(GetAllListAsync));
    }
}

protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue arguments = null)
{
    await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments));
}

 

  protected virtual ClientProxyRequestContext BuildHttpProxyClientProxyContext(string methodName, ClientProxyRequestTypeValue arguments = null)
    {
        if (arguments == null)
        {
            arguments = new ClientProxyRequestTypeValue();
        }

        var methodUniqueName = $"{typeof(TService).FullName}.{methodName}.{string.Join("-", arguments.Values.Select(x => TypeHelper.GetFullNameHandlingNullableAndGenerics(x.Key)))}"; 
        var action = ClientProxyApiDescriptionFinder.FindAction(methodUniqueName); //获取调用方法的API描述信息
        if (action == null)
        {
            throw new AbpException($"The API description of the {typeof(TService).FullName}.{methodName} method was not found!");
        }

        var actionArguments = action.Parameters.GroupBy(x => x.NameOnMethod).ToList();
        if (action.SupportedVersions.Any()) 
        {   
            //TODO: make names configurable
            actionArguments.RemoveAll(x => x.Key == "api-version" || x.Key == "apiVersion");
        }

        return new ClientProxyRequestContext(   //封装未远程调用上下文
            action,
                actionArguments
                .Select((x, i) => new KeyValuePair<string, object>(x.Key, arguments.Values[i].Value))
                .ToDictionary(x => x.Key, x => x.Value),
            typeof(TService));
    }
 
ClientProxyApiDescriptionFinder IClientProxyApiDescriptionFinder 的实例。默认实现是 ClientProxyApiDescriptionFinder
该实例初始化时调用 GetApplicationApiDescriptionModel 方法从虚拟文件系统中读取所有的 *generate-proxy.json 文件获取到API描述信息。
 
 private ApplicationApiDescriptionModel GetApplicationApiDescriptionModel()
    {
        var applicationApiDescription = ApplicationApiDescriptionModel.Create();
        var fileInfoList = new List<IFileInfo>();
        GetGenerateProxyFileInfos(fileInfoList);

        foreach (var fileInfo in fileInfoList)
        {
            using (var streamReader = new StreamReader(fileInfo.CreateReadStream()))
            {
                var content = streamReader.ReadToEnd();

                var subApplicationApiDescription = JsonSerializer.Deserialize<ApplicationApiDescriptionModel>(content);

                foreach (var module in subApplicationApiDescription.Modules)
                {
                    if (!applicationApiDescription.Modules.ContainsKey(module.Key))
                    {
                        applicationApiDescription.AddModule(module.Value);
                    }
                }
            }
        }

        return applicationApiDescription;
    }

    private void GetGenerateProxyFileInfos(List<IFileInfo> fileInfoList, string path = "")
    {
        foreach (var directoryContent in VirtualFileProvider.GetDirectoryContents(path))
        {
            if (directoryContent.IsDirectory)
            {
                GetGenerateProxyFileInfos(fileInfoList, directoryContent.PhysicalPath);
            }
            else
            {
                if (directoryContent.Name.EndsWith("generate-proxy.json"))
                {
                    fileInfoList.Add(VirtualFileProvider.GetFileInfo(directoryContent.GetVirtualOrPhysicalPathOrNull()));
                }
            }
        }
    }
后面 RequestAsync 方法就跟动态API客户端一样了。
 

要点

1.因为已经事先生成好API描述文件,所以避免了动态API客户端要点2的问题。但是在生成时也需要遵循要点2。

 文章来源地址https://www.toymoban.com/news/detail-455844.html

总结

动态API客户端

1.注册动态代理传入的接口层程序集和远程服务名称,可以实现将远程调用类型与远程服务名称绑定在一起的作用。这样,在使用具体的服务类型进行远程调用时,就能够根据远程服务名称快速找到对应的服务地址。
2.在远程调用时,首先会调用相应服务的 api/abp/api-definition 接口获取到该服务的所有API描述信息,后将其封装成远程调用上下文,接着拼接完整的Url,添加请求头与认证信息(不允许匿名访问)就可以进行http请求了。

静态API客户端

1.通过abp cli工具生成调用类跟API描述文件,在远程调用时,通过*generate-proxy.json 文件获取到相应接口的API描述信息,往后跟动态API客户端流程一样。
 

最后

写到最后,文章开头的疑问应该解决了吗?

ABPVNEXT框架 QQ交流群:655362692

 

到了这里,关于Abp Vnext 动态(静态)API客户端源码解析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 阿里云AliYun物联网平台使用-客户端API获取设备传感数据

            上一篇文章中,已经实现了虚拟数据上云,本文我们将进行上位机客户端的开发,即通过调用阿里云IOT物联网云平台的SDK,开发能获取传感器的遥感数据。         调用API需要用户的AccessKey Secret,这意味着客户端将取得主体账号的所有权限。为了防止恶意用户通过

    2024年02月16日
    浏览(50)
  • 分享一个由rust实现的openai api服务端+Android客户端

    官方网页存在经常中途断开的问题. 经常使用不同 ip 登录 openai 帐号可能会导致封号. 使用开源项目 chatgpt-web 搭建过一个网页端,目前已被DNS污染, 体验 GitHub Copilot . 已经使用了 rust 语言一段时间,打算用它写个服务端练手. 服务端 技术栈 rust Rust是一种系统级编程语言,由Mozil

    2024年02月13日
    浏览(54)
  • Spring Cloud【Config客户端配置与测试、Config客户端之动态刷新 、什么是Spring Cloud Bus、Docker安装RabbitMQ】(十)

      目录 分布式配置中心_Config客户端配置与测试 为什么要引入bootstrap 

    2024年02月15日
    浏览(43)
  • Linux ntpdate命令介绍(校时客户端)(ntp客户端)ubuntu离线安装ntpdate(未封装、高端口号)(知名端口、注册端口、动态端口/私有端口)

    NTPdate是一种在Linux和类Unix系统中同步网络时间协议(NTP)服务器时间的命令行程序。它可以让你的系统时钟与互联网上的标准时间服务器保持一致,从而确保系统时间的准确性。 查询ntp服务器时间: 在开始使用NTPdate之前,我们需要先在系统中安装它。以下是在不同系统中安

    2024年02月04日
    浏览(47)
  • minio客户端上提示 S3 API Requests must be made to API port

    1、看提示 “S3 API Requests must be made to API port”,说明是由端口号引发的问题,查看mc绑定的服务端口与容器的映射端口(通常为9000)是否一致,如果不同就取消绑定,然后重新绑定并设置端口为9000

    2024年02月11日
    浏览(39)
  • Elasticsearch8.x版本Java客户端Elasticsearch Java API Client中常用API练习

    在Es7.15版本之后,es官方将它的高级客户端RestHighLevelClient标记为弃用状态。同时推出了全新的java API客户端Elasticsearch Java API Client,该客户端也将在Elasticsearch8.0及以后版本中成为官方推荐使用的客户端。 Elasticsearch Java API Client支持除Vector title search API和Find structure API之外的所有

    2024年04月11日
    浏览(49)
  • Nacos源码 (5) Grpc服务端和客户端

    Nacos 2.x在服务端与客户端直接增加了GRPC通信方式,本文通过2.0.2版本源码,简单分析GRPC通信方式: 服务器启动 客户端连接 客户端心跳 服务器监控检查 api/src/main/proto/nacos_grpc_service.proto文件: 文件定义了通信层的service和message结构,业务层请求响应的序列化和反序列化是Na

    2024年02月10日
    浏览(48)
  • client-go源码结构及客户端对象

    G  Goup 资源组,包含一组资源操作的集合 V Version 资源版本,用于区分不同API的稳定程度及兼容性 R Resource 资源信息,用于区分不同的资源API K Kind 资源对象类型,每个资源对象都需要Kind来区分它自身代表的资源类型 (1)通过 GVR 可以构造 REST Api  进行接口调用,而 GVK 可以

    2024年04月26日
    浏览(42)
  • ONVIF协议网络摄像机(IPC)客户端程序开发使用ONVIF框架代码(C++)生成静态库04-->Windows

    先说一下,为什么不像前面Linux生成动态库那样去开发Onvif。因为Onvif的源码是没有那些__declspec(dlleXPort)指令,所以当你导出dll时,你会发现没有xxx.lib文件产生,在windows下你就没办法隐式调用dll了,除非你显示调用dll,即在程序中使用LoadLibrary()一个一个将要用到的函数加

    2023年04月08日
    浏览(58)
  • Cloudreve CR+安卓客户端 V1.0.9.8源码

    搭建教程:CR+商业版搭建教程.zip 我忘记这个教程版本是几点几的了,但是安装步骤一般都是大同小异的 需要修改的地方为 CR+ 1.0.9.8升级版源码清玖云盘resconfigappinit.lua 注意,本版本并不适用于现在新版本的cloudreve,请自行修改api适配最新版的cloudreve 我没找到这个版本的

    2024年02月11日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包