在很多时候我们在不同的服务之间需要通过HttpClient进行及时通讯,在我们的代码中我们会创建自己的HttpClient对象然后去跨领域额进行数据的交互,但是往往由于一个项目有多个人开发所以在开发中没有人经常会因为不同的业务请求去写不同的代码,然后就会造成各种风格的HttpClient的跨域请求,最重要的是由于每个人对HttpClient的理解程度不同所以写出来的代码可能质量上会有参差不齐,即使代码能够达到要求往往也显得非常臃肿,重复高我们在正式介绍Refit这个项目之前,我们来看看我们在项目中常用的调用方式,后面再来介绍这种处理方式的弊端以及后面集成了Refit以后我们代码的质量能够有哪些程度的提高。
常规创建方式
在常规的方式中我们一般使用IHttpClientFactory来创建HttpClient对象,然后使用这个对象来发送和接收消息,我们先来看下面的这个例子。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using System.Web; using Abp.Domain.Services; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace Sunlight.Dms.Parts.Domain.Web { /// <summary> /// HttpClient的帮助类 /// </summary> public class DcsPartClientService : DomainService { private readonly HttpClient _httpClient; private readonly ILogger<DcsPartClientService> _loggerHelper; public DcsPartClientService(IHttpClientFactory httpClientFactory, ILogger<DcsPartClientService> loggerHelper) { _loggerHelper = loggerHelper; _httpClient = httpClientFactory.CreateClient(PartsConsts.DcsPartClientName); if (_httpClient.BaseAddress == null) { throw new ArgumentNullException(nameof(httpClientFactory), $"没有配置名称为 {PartsConsts.DcsPartClientName} 的HttpClient,或者接口服务的地址为空"); } } /// <summary> /// Post请求返回实体 /// </summary> /// <param name="relativeUrl">请求相对路径</param> /// <param name="postObj">请求数据</param> /// <returns>实体T</returns> public async Task<List<T>> PostResponse<T>(string relativeUrl, object postObj) where T : class { var postData = JsonConvert.SerializeObject(postObj); _httpClient.DefaultRequestHeaders.Add("user-agent", "Dcs-Parts"); _httpClient.CancelPendingRequests(); _httpClient.DefaultRequestHeaders.Clear(); HttpContent httpContent = new StringContent(postData); httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var result = default(List<T>); var response = await _httpClient.PostAsync(_httpClient.BaseAddress + relativeUrl, httpContent); if (response.StatusCode == HttpStatusCode.NotFound) { throw new ValidationException("找不到对应的DcsParts服务"); } var responseContent = await response.Content.ReadAsAsync<ReceiveResponseBody<List<T>>>(); if (response.IsSuccessStatusCode) { result = responseContent?.Payload; } else { if (!string.IsNullOrWhiteSpace(responseContent?.Message)) { throw new ValidationException(responseContent.Message); } _loggerHelper.LogDebug($"请求返回结果:{0} 请求内容:{1}", response.StatusCode, postData); } return await Task.FromResult(result); } public async Task<List<T>> GetResponse<T>(string relativeUrl, object queryObj) where T : class { var queryData = ModelToUriQueryParam(queryObj); _httpClient.DefaultRequestHeaders.Add("user-agent", "Dcs-Parts"); _httpClient.CancelPendingRequests(); _httpClient.DefaultRequestHeaders.Clear(); _httpClient.DefaultRequestHeaders.Add("accept", "application/json"); var result = default(List<T>); var response = await _httpClient.GetAsync(_httpClient.BaseAddress + relativeUrl + queryData); if (response.StatusCode == HttpStatusCode.NotFound) { throw new ValidationException("找不到对应的DcsParts服务"); } var responseContent = await response.Content.ReadAsAsync<ReceiveResponseBody<List<T>>>(); if (response.IsSuccessStatusCode) { result = responseContent?.Payload; } else { if (!string.IsNullOrWhiteSpace(responseContent?.Message)) { throw new ValidationException(responseContent.Message); } } return await Task.FromResult(result); } private string ModelToUriQueryParam<T>(T t, string url = "") { var properties = t.GetType().GetProperties(); var sb = new StringBuilder(); sb.Append(url); sb.Append("?"); foreach (var p in properties) { var v = p.GetValue(t, null); if (v == null) continue; sb.Append(p.Name); sb.Append("="); sb.Append(HttpUtility.UrlEncode(v.ToString())); sb.Append("&"); } sb.Remove(sb.Length - 1, 1); return sb.ToString(); } } public class ReceiveResponseBody<T> where T : class { public string Message { get; set; } public T Payload { get; set; } } public class ReceiveResponseBody { public string Message { get; set; } } }
1.1 注入IHttpClientFactory对象
在这个过程中我们通过构造函数来注入IHttpClientFactory接口,然后用这个接口的CreateClient方法来创建一个唯一的HttpClient对象,在这里我们一般都会同步注入ILogger接口来记录日志信息从而便于我们排查线上问题,这里我们在CreateClient方法中传入了一个字符串类型的参数用于标记自己创建的HttpClient对象的唯一性。这里我们可以看到在构造函数中我们会去判断当前创建的HttpClient的BaseAddress,如果没有这个基地址那么程序会直接抛出错误提示,那么问题来了我们的HttpClient的BaseAddress到底在哪里配置呢?熟悉Asp.Net Core机制的朋友肯定一下子就会想到在Startup类中配置,那么我们来看看需要怎么配置。
1.2 配置HttpClient的BaseAddress
public IServiceProvider ConfigureServices(IServiceCollection services) { //dcs.part服务 services.AddHttpClient(PartsConsts.DcsPartClientName, config => { config.BaseAddress = new Uri(_appConfiguration["DependencyServices:DcsParts"]); config.Timeout = TimeSpan.FromSeconds(60); }); }
这里我只是简要截取了一小段内容,这里我们看到AddHttpClient的第一个参数也是一个字符串常量,这个常量应该是和IHttpClientFactory的CreateClient的方法中的那个常量保持绝对的一直,只有这样我们才能够标识唯一的标识一个HttpClient对象,创建完了之后我们就能够在这个里面去配置这个HttpClient的各种参数了,包括基地这、超时时间......等等,当然这个基地址我们是配置在appsetting.json中的,具体的配置如下所示。
"DependencyServices": { "BlobStorage": "http://blob-storage/", "DcsParts": "http://dcs-parts/", "DmsAfterSales": "http://dms-after-sales/" }
有了这些我们就能够具备创建一个HttpClient对象的条件了,后面我们来看看我们怎么使用这个HttpClient进行发送和接收数据。
1.3 HttpClient进行数据的发送和接收
/// <summary> /// Post请求返回实体 /// </summary> /// <param name="relativeUrl">请求相对路径</param> /// <param name="postObj">请求数据</param> /// <returns>实体T</returns> public async Task<List<T>> PostResponse<T>(string relativeUrl, object postObj) where T : class { var postData = JsonConvert.SerializeObject(postObj); _httpClient.DefaultRequestHeaders.Add("user-agent", "Dcs-Parts"); _httpClient.CancelPendingRequests(); _httpClient.DefaultRequestHeaders.Clear(); HttpContent httpContent = new StringContent(postData); httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var result = default(List<T>); var response = await _httpClient.PostAsync(_httpClient.BaseAddress + relativeUrl, httpContent); if (response.StatusCode == HttpStatusCode.NotFound) { throw new ValidationException("找不到对应的DcsParts服务"); } var responseContent = await response.Content.ReadAsAsync<ReceiveResponseBody<List<T>>>(); if (response.IsSuccessStatusCode) { result = responseContent?.Payload; } else { if (!string.IsNullOrWhiteSpace(responseContent?.Message)) { throw new ValidationException(responseContent.Message); } _loggerHelper.LogDebug($"请求返回结果:{0} 请求内容:{1}", response.StatusCode, postData); } return await Task.FromResult(result); }
在上面的代码中我们模拟了一个Post请求,请求完成以后我们再使用ReadAsAsync的方法来异步接收另外一个域中的数据,然后我们根据返回的StatusCode来抛出不同的错误提示,并记录相关的日志信息并返回最终Post请求的结果,进而完成整个过程,在这个中间我们发送请求的时候需要注意一下内容:1 最终的完整版地址=BaseAddress+RelativeAddress,基地址是在appsetting.json中进行配置的,RelativeAddress是我们请求不同域的时候的相对地址,这个需要我们根据实际的业务来进行配置。2 请求的对象是我们将数据对象序列化成json后的结果,这两点需要特别注意。