如何用HttpClientFactory创建高效多客户端HttpClient并实现代码复用?
问题描述
我用HttpClientFactory创建HttpClient,现在要对接多个外部客户端,想把重复的请求逻辑集中管理减少冗余,但不确定高请求量场景下这种中心化实现是否符合最佳实践,同时想知道怎么在保证可扩展性的同时避免重复代码。
当前代码里ApiClientOne和ApiClientTwo有完全相同的私有Request方法,我想把这个方法移到公共位置让N个客户端调用,有三个疑问:
- 将
async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null)移到公共位置供多客户端调用,是否具备良好的可扩展性? - 是否有更优的实现方案?
- 是否应该保持当前的独立实现方式?
代码示例
Program.cs
.ConfigureServices((_, services) => { services.AddApiClientOne(config); services.AddApiClientTwo(config); })
ServiceContainer.cs
namespace Configuration { [ExcludeFromCodeCoverage] public static class ServiceContainer { public static void AddApiClientTwo(this IServiceCollection services, IConfiguration configuration) { services.AddHttpClient<IApiClientTwo, ApiClientTwo>((serviceProvider, httpClient) => { httpClient.BaseAddress = new Uri(configuration.GetSettings("Services:ApiClientTwo:BaseUrl", "")); httpClient.Timeout = TimeSpan.FromSeconds(30); httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", serviceProvider.GetRequiredService<IInternalAuthClient>().GetTokenAsync() .GetAwaiter().GetResult()); }); } public static void AddApiClientOne(this IServiceCollection services, IConfiguration configuration) { services.AddHttpClient<IApiClientOne, ApiClientOne>("ApiClientOne", options => { options.BaseAddress = new Uri(configuration.GetSettings("Services:ClientOne:BaseUrl", string.Empty, false)); options.DefaultRequestHeaders.Add("clientId", configuration[Configurations.ClientOneClientId]); options.SetBasicAuthentication(configuration[Configurations.ClientOneUsername], configuration[Configurations.ClientOnePassword]); }); } } }
ApiClientOne.cs
namespace Clients.ApiClientOne; public class ApiClientOne : IApiClientOne { private readonly ILogger<ApiClientOne> _logger; private readonly HttpClient _httpClient; private Lazy<JsonSerializerOptions> _settings; public ApiClientOne(HttpClient httpClient, ILogger<ApiClientOne> logger) { _httpClient = httpClient; _logger = logger; _settings = new Lazy<JsonSerializerOptions>(CreateSerializerSettings); } public async Task<GetSomethingResponse> GetSomething(string value) { var response = await Request<GetSomethingResponse>(HttpMethod.Get, $"/something/{value}"); return response; } private JsonSerializerOptions CreateSerializerSettings() { var settings = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return settings; } private async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null) { var client = _httpClient; var disposeClient = false; using var request = new HttpRequestMessage(); var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value)); content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); request.Content = content_; request.Method = method; request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); var disposeResponse_ = true; try { var content = await response.Content.ReadAsStringAsync(); var headers = Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value); if (response.Content != null && response.Content.Headers != null) { foreach (var item in response.Content.Headers) headers[item.Key] = item.Value; } if (!response.IsSuccessStatusCode) { var errorMessage = $"some error."; _logger.Error($"{errorMessage} {{ @content }}", args: new object[] {content}); throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode); } return JsonSerializer.Deserialize<T>(content); } catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int) ex.StatusCode && (int) ex.StatusCode < 600) { throw new DependencyFailureApiException(ex.Message); } finally { if (disposeResponse_) { response.Dispose(); client.Dispose(); } } } }
ApiClientTwo.cs
namespace Clients.ApiClientTwo; public class ApiClientTwo : IApiClientTwo { private readonly ILogger<ApiClientTwo> _logger; private readonly HttpClient _httpClient; private Lazy<JsonSerializerOptions> _settings; public ApiClientTwo(HttpClient httpClient, ILogger<ApiClientTwo> logger) { _httpClient = httpClient; _logger = logger; _settings = new Lazy<JsonSerializerOptions>(CreateSerializerSettings); } public async Task<PostSomethingResponse> PostSomething(MyModel value) { var response = await Request<PostSomethingResponse>(HttpMethod.Post, $"/postSomething", value); return response; } private JsonSerializerOptions CreateSerializerSettings() { var settings = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return settings; } private async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null) { var client = _httpClient; var disposeClient = false; using var request = new HttpRequestMessage(); var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value)); content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); request.Content = content_; request.Method = method; request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); var disposeResponse_ = true; try { var content = await response.Content.ReadAsStringAsync(); var headers = Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value); if (response.Content != null && response.Content.Headers != null) { foreach (var item in response.Content.Headers) headers[item.Key] = item.Value; } if (!response.IsSuccessStatusCode) { var errorMessage = $"some error."; _logger.Error($"{errorMessage} {{ @content }}", args: new object[] { content }); throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode); } return JsonSerializer.Deserialize<T>(content); } catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int)ex.StatusCode && (int)ex.StatusCode < 600) { throw new DependencyFailureApiException(ex.Message); } finally { if (disposeResponse_) { response.Dispose(); client.Dispose(); } } } }
问题解答
1. 公共Request方法的可扩展性
把Request方法移到公共位置具备良好的可扩展性,关键是选对实现方式:
- 用抽象基类:定义
BaseApiClient封装通用逻辑(Request方法、序列化配置、Logger、HttpClient),子类继承后只需实现业务接口方法。这种方式允许子类重写部分逻辑(比如特殊序列化规则、自定义错误处理),灵活性足够覆盖绝大多数场景。 - 用静态扩展方法:把Request做成HttpClient的扩展,传入Logger和序列化配置即可。这种方式更轻量,但灵活性稍弱,适合所有客户端逻辑完全统一的场景。
高请求量场景下,这种中心化实现不会有性能问题——HttpClientFactory本身就是为高并发设计的,公共方法只是复用逻辑,不会影响连接池管理,反而因为代码统一,更容易批量优化(比如添加统一的重试、熔断策略)。
2. 更优的实现方案
推荐两种方案,按实用性排序:
方案一:抽象基类+依赖注入
定义抽象基类封装通用逻辑,子类专注业务实现:
public abstract class BaseApiClient<TClient> { protected readonly HttpClient _httpClient; protected readonly ILogger<TClient> _logger; protected readonly Lazy<JsonSerializerOptions> _serializerOptions; protected BaseApiClient(HttpClient httpClient, ILogger<TClient> logger) { _httpClient = httpClient; _logger = logger; _serializerOptions = new Lazy<JsonSerializerOptions>(CreateSerializerSettings); } protected virtual JsonSerializerOptions CreateSerializerSettings() { return new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; } protected async Task<TResponse> Request<TResponse>(HttpMethod method, string endpoint, object payload = null) { using var request = new HttpRequestMessage(method, endpoint); if (payload != null) { var content = new StringContent(JsonSerializer.Serialize(payload, _serializerOptions.Value)); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); request.Content = content; } request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); try { var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { var errorMessage = $"请求失败,状态码:{(int)response.StatusCode}"; _logger.LogError($"{errorMessage},内容:{content}"); throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode); } return JsonSerializer.Deserialize<TResponse>(content, _serializerOptions.Value); } catch (HttpRequestException ex) when (ex.StatusCode.HasValue && (int)ex.StatusCode >= 500 && (int)ex.StatusCode < 600) { throw new DependencyFailureApiException(ex.Message); } } }
子类继承基类后只需实现业务方法:
public class ApiClientOne : BaseApiClient<ApiClientOne>, IApiClientOne { public ApiClientOne(HttpClient httpClient, ILogger<ApiClientOne> logger) : base(httpClient, logger) { } public async Task<GetSomethingResponse> GetSomething(string value) { return await Request<GetSomethingResponse>(HttpMethod.Get, $"/something/{value}"); } }
重要提醒:原代码里的client.Dispose()必须删掉!HttpClientFactory管理的HttpClient不能手动释放,否则会破坏连接池,导致高并发下性能急剧下降。
方案二:自定义DelegatingHandler处理横切逻辑
如果需要统一处理请求日志、重试、认证等横切逻辑,可以自定义委托处理程序:
public class ApiClientLoggingHandler : DelegatingHandler { private readonly ILogger<ApiClientLoggingHandler> _logger; public ApiClientLoggingHandler(ILogger<ApiClientLoggingHandler> logger) { _logger = logger; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { _logger.LogInformation("发送请求:{Method} {Uri}", request.Method, request.RequestUri); var response = await base.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); _logger.LogError("请求失败:{StatusCode} {Content}", response.StatusCode, content); } return response; } }
注册HttpClient时添加该Handler:
services.AddHttpClient<IApiClientOne, ApiClientOne>() .AddHttpMessageHandler<ApiClientLoggingHandler>();
这种方式适合把横切逻辑和业务逻辑分离,配合基类使用效果最佳。
3. 是否保持独立实现?
不建议保持当前的独立实现方式:
- 重复代码会导致维护成本极高,比如修改错误处理逻辑、序列化配置时,需要在N个客户端里逐一修改,容易遗漏。
- 高请求量场景下,统一的逻辑更容易批量优化(比如添加统一的重试策略、超时控制),独立实现很难做到逻辑一致。
- 唯一的例外是某个客户端的请求逻辑完全特殊,和其他客户端没有任何共性,但这种情况极少,大部分场景下都能提取通用逻辑。
内容的提问来源于stack exchange,提问作者GThree
相关产品推荐
相关产品推荐

