Abp Vnext 動態(靜態)API使用者端原始碼解析

2023-05-23 12:01:08
根據以往的經驗,通過介面遠端呼叫服務的原理大致如下:
  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描述資訊。

 

二.靜態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。

 

總結

動態API使用者端

1.註冊動態代理傳入的介面層程式集和遠端服務名稱,可以實現將遠端呼叫型別與遠端服務名稱繫結在一起的作用。這樣,在使用具體的服務型別進行遠端呼叫時,就能夠根據遠端服務名稱快速找到對應的服務地址。
2.在遠端呼叫時,首先會呼叫相應服務的 api/abp/api-definition 介面獲取到該服務的所有API描述資訊,後將其封裝成遠端呼叫上下文,接著拼接完整的Url,新增請求頭與認證資訊(不允許匿名存取)就可以進行http請求了。

靜態API使用者端

1.通過abp cli工具生成呼叫類跟API描述檔案,在遠端呼叫時,通過*generate-proxy.json 檔案獲取到相應介面的API描述資訊,往後跟動態API使用者端流程一樣。
 

最後

寫到最後,文章開頭的疑問應該解決了嗎?

ABPVNEXT框架 QQ交流群:655362692