news 2026/10/17 5:50:56

HttpClient消息管道探秘:用DelegatingHandler统一处理日志、认证与重试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HttpClient消息管道探秘:用DelegatingHandler统一处理日志、认证与重试

我最早接触DelegatingHandler,是在排查一组接口超时问题的时候。日志打了一堆,状态码看着也正常,可就是有几个请求慢得离谱,最后定位到是某个调用链路上重试逻辑写得太分散,每个请求都要手工处理认证头、日志、超时控制,代码又乱又难排查。后来把这一堆乱七八糟的横切逻辑全部收敛进DelegatingHandler之后,整个调用链瞬间清爽了。如果你也在用HttpClient对接第三方API、做微服务间调用,或者有大量重复的请求头和认证逻辑需要统一处理,这篇文章应该能帮你少踩不少坑。

1. 先搞清楚DelegatingHandler到底是个什么角色

1.1 从HttpClient的消息管道说起

很多同学第一次接触HttpClient,就是直接 new 一个实例然后调用 GetAsync、PostAsync,至于请求发出之后经历了什么,基本是个黑盒。实际上,HttpClient 每一次请求在真正发到网络之前,会经过一条消息管道,管道里是一串按顺序排列的 HttpMessageHandler。请求从最外层进去,逐层向内传递,直到最底层的 HttpClientHandler 真正建立连接、发送字节流;响应再沿着同一路径反向流出来。

DelegatingHandler 就是这条管道里的“中间件”。它本身继承自 HttpMessageHandler,需要接收一个 InnerHandler,SendAsync 方法里你可以决定是直接短路返回,还是调用 base.SendAsync 把请求往下传。听起来很像ASP.NET里的中间件管道或者洋葱模型,只不过HttpClient的这套机制诞生得更早,用法也简单直接得多。

1.2 DelegatingHandler解决了什么问题

你在 HttpClient 上重复写的那些代码,基本都属于横切关注点。举例来说,每个请求都要附加 AccessToken,你要么在调用处手工塞 Header,要么写一个包装类每次都处理一遍;每个请求都要打日志,你就得在业务代码里到处埋点;每个请求都要做超时重试,你很可能已经写过三份不同的重试工具类了。

DelegatingHandler 的核心价值就是把这类逻辑从业务代码中剥离出来,变成一个个独立、可组合的处理器。你可以像搭积木一样把它们串起来,日志、认证、重试、熔断各管一段,彼此不干扰。更重要的是,这个模型是官方推荐的扩展点,不是你自创的私有方案,后续接手你代码的人不需要额外学习门槛。

1.3 一个最简单的DelegatingHandler长什么样

我先把最基础的结构贴出来,方便你建立直观印象。

public class SimpleHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { // 请求阶段:可以在发出去之前做点什么 Console.WriteLine($"Request: {request.Method} {request.RequestUri}"); // 调用内层Handler,继续把请求往下传 var response = await base.SendAsync(request, cancellationToken); // 响应阶段:可以在这里处理结果 Console.WriteLine($"Response: {(int)response.StatusCode}"); return response; } }

就这么简单。关键就两个点:base.SendAsync 是往下传的唯一途径,不调用它,请求就没有下一层处理器,也就永远不会真正发出;它在 await 前后的代码分别对应请求和响应两个阶段。后面所有复杂的用法,都是在这个基础上叠加出来的。

2. Handler链的构建顺序和执行顺序,最容易翻车的地方

2.1 链式装配的两种典型写法

DelegatingHandler 的装配方式本质上就是逐层指定 InnerHandler。手动方式是把一串 Handler 用属性嵌套起来,最后塞给 HttpClient 的构造函数:

var handler = new LoggingHandler { InnerHandler = new AuthHandler { InnerHandler = new RetryHandler { InnerHandler = new HttpClientHandler() } } }; var client = new HttpClient(handler);

在 ASP.NET Core 里更推荐用 IHttpClientFactory 的方式注册,代码会简洁很多:

services.AddHttpClient("demo") .AddHttpMessageHandler<LoggingHandler>() .AddHttpMessageHandler<AuthHandler>() .AddHttpMessageHandler<RetryHandler>();

这两种写法本质是一样的,AddHttpMessageHandler 的注册顺序,就对应链上由外到内的排列顺序。

2.2 执行顺序:请求正序,响应逆序

这是我见过出错最多的地方。很多同学以为响应回来的顺序和请求一样,结果发现响应先经过最内层再回到最外层,完全反了。

如果你注册了 LoggingHandler → AuthHandler → RetryHandler 这样一串,一次请求的完整路径是:

  1. 请求先进入 LoggingHandler
  2. LoggingHandler 调用 base.SendAsync,请求进入 AuthHandler
  3. AuthHandler 调用 base.SendAsync,请求进入 RetryHandler
  4. RetryHandler 调用 base.SendAsync,请求进入 HttpClientHandler,真正发到网络
  5. 响应返回后先经过 RetryHandler,再经过 AuthHandler,最后经过 LoggingHandler

所以你要在日志里记录响应耗时,日志Handler必须放在最外层。如果你把日志Handler放在内层,虽然也能记录,但计时范围不包括外面其他Handler消耗的时间,统计出来的数据就是不完整的。我在一个项目里就是因为把超时Handler放在日志里面,导致日志显示全部正常,实际却有请求已经重试了几次,排查了半天才发现是顺序搞反了。

2.3 短路机制:不调用base.SendAsync会怎样

DelegatingHandler 可以做“短路”操作。比如你想在某个Handler里判断请求是不是合法,如果不合法直接返回一个401,不让请求继续往下走,那就可以不调用 base.SendAsync:

public class BlockHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Headers.Contains("Blocked")) { return new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden); } return await base.SendAsync(request, cancellationToken); } }

注意,一旦短路,请求不会进入内层Handler,但响应仍然会沿着原路返回给外层的Handler。这个行为在链路中表现得像提前return,理解这个机制后,你就知道为什么可以在认证Handler里做接口级别的访问控制了。

3. 三个最实用的实战场景:日志、认证、重试

3.1 场景一:全链路请求日志Handler

日志Handler基本是每个项目都应该有的基础设施。它要做的事情很明确:记录请求方法、URL、请求头、请求体,响应状态码、响应耗时,以及异常信息。写的时候有几个细节需要注意。

请求体和响应体都是 Stream,而且默认只能读取一次。你在Handler里读了,后续的业务代码再读就是空的了。所以要么读取后用 Position=0 重置流位置,要么像 ASP.NET Core 那样做缓冲。我习惯在读取时先转成字符串再重新赋值流位置,确保下游能正常读取。

public class LoggingHandler : DelegatingHandler { private readonly ILogger<LoggingHandler> _logger; public LoggingHandler(ILogger<LoggingHandler> logger) { _logger = logger; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); string requestBody = string.Empty; if (request.Content != null) { requestBody = await request.Content.ReadAsStringAsync(cancellationToken); } _logger.LogInformation("Request {Method} {Url} Body: {Body}", request.Method, request.RequestUri, requestBody); var response = await base.SendAsync(request, cancellationToken); stopwatch.Stop(); string responseBody = string.Empty; if (response.Content != null) { responseBody = await response.Content.ReadAsStringAsync(cancellationToken); } _logger.LogInformation("Response {StatusCode} {Url} Duration: {Duration}ms Body: {Body}", (int)response.StatusCode, request.RequestUri, stopwatch.ElapsedMilliseconds, responseBody); return response; } }

这里有一个容易忽略的点:ReadAsStringAsync 读完之后,响应体的 Content 流已经被消耗了。如果下游业务代码还要读响应内容,你最好先读取并缓存,或者把待读内容重新赋给 response.Content。我一般在日志场景中不保留响应体,只记录前若干字符,避免日志文件膨胀得太快。

3.2 场景二:自动附加Token和请求头的认证Handler

最常见的认证逻辑就是从某个地方拿到Token,然后附加到每个请求的 Authorization Header。不同项目的Token来源千差万别,有静态配置的,有OAuth动态获取的,有从请求上下文里拿的。统一放到Handler里,业务代码就完全不需要关心认证细节。

public class AuthHandler : DelegatingHandler { private readonly ITokenProvider _tokenProvider; public AuthHandler(ITokenProvider tokenProvider) { _tokenProvider = tokenProvider; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var token = await _tokenProvider.GetTokenAsync(cancellationToken); if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); } }

这个Handler有两点要注意。第一,TokenProvider 如果是每次请求都获取新Token的,性能会很难看,我建议在Provider内部做缓存和刷新机制,比如提前30秒刷新,避免并发穿透;第二,如果你的请求已经手动设置了 Authorization Header,要处理好覆盖还是保留的逻辑。我在实际项目中遇到过一个隐藏Bug:某个业务方坚持在调用处手动设置Header,结果认证Handler又覆盖了一遍,导致带上了一个完全不同的Token,排查了很久才发现是两个来源不一致。

如果你对接的是内部微服务,或者需要内部服务间传递用户上下文,可以在这个Handler里追加 X-User-Id、X-Trace-Id 这类Header,实现链路追踪字段的自动传递,这对排查问题价值巨大。

3.3 场景三:超时和重试合一的可靠性Handler

重试是另一个高频场景,但坑也最多。我见过不少人直接循环发送同一个 HttpRequestMessage 实例,结果第二次发送直接抛异常,因为 HttpRequestMessage 本身是不可重用的。所以,重试Handler里必须先克隆请求,再发送克隆体。

public class RetryHandler : DelegatingHandler { private static readonly TimeSpan[] RetryDelays = new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(5) }; private readonly int _maxRetries = RetryDelays.Length; public RetryHandler(int maxRetries) { _maxRetries = maxRetries; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { for (int attempt = 0; ; attempt++) { var clonedRequest = CloneRequest(request); var response = await base.SendAsync(clonedRequest, cancellationToken); if (!ShouldRetry(attempt, response.StatusCode)) { return response; } if (attempt >= _maxRetries - 1) { return response; } await Task.Delay(GetDelay(attempt), cancellationToken); } } private static bool ShouldRetry(int attempt, HttpStatusCode statusCode) { return statusCode == HttpStatusCode.RequestTimeout || statusCode == HttpStatusCode.TooManyRequests || (int)statusCode >= 500; } private static TimeSpan GetDelay(int attempt) { return RetryDelays[Math.Min(attempt, RetryDelays.Length - 1)]; } private static HttpRequestMessage CloneRequest(HttpRequestMessage request) { var clone = new HttpRequestMessage(request.Method, request.RequestUri); clone.Version = request.Version; foreach (var header in request.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } foreach (var property in request.Options) { clone.Options.TryAdd(property.Key, property.Value); } clone.Content = request.Content; return clone; } }

这里有几个地方值得展开讲讲。

ShouldRetry 的判断逻辑,我建议对 GET 这类幂等请求才重试,对 POST 这种非幂等请求要格外谨慎。如果你重试了一个已经处理成功的POST请求,就可能造成重复下单或重复扣款。我见过的一个项目就是因为盲目重试,导致下游生成了多条重复数据,最后只能靠人工处理。稳妥的方案是:对于非幂等操作,要么不重试,要么让下游接口保证幂等性(比如带上请求ID做去重)。

CancellationToken 的处理也非常关键。Task.Delay 必须传入 cancellationToken,否则用户取消操作时重试还会继续等待,白白消耗时间和资源。更重要的,我在实际排查中发现,很多人会漏掉 catch OperationCanceledException 的逻辑,导致取消操作时会抛异常。重试Handler最好在捕获取消异常后直接重新抛出,让上层统一处理。

3.4 把这三个Handler串起来之后的效果

当你把上面三个Handler按顺序串起来,一次接口调用的完整流程就变成:日志记录请求 → 附加认证Token → 尝试发送并自动重试 → 记录响应和耗时。

这样的设计让业务代码只保留一行代码:

var response = await client.GetAsync(url, cancellationToken);

所有的横切逻辑全部被隔离在管道里,业务层只关心自己那部分。新增一个横切需求,不需要改任何调用点,只需要加一个新的Handler并注册进去。这个价值在项目规模变大后会体现得格外明显。

4. 让你代码更优雅的四个高阶玩法

4.1 用闭包Handler减少类型数量

有些小场景,比如就想给某个HttpClient加一个简单的Header,不值得专门写一个Handler类。你可以用匿名委托的方式快速创建一个:

var handler = new DelegatingHandler { InnerHandler = new HttpClientHandler(), InnerHandler2 = null // 这里其实不对,看下面正确写法 };

在上面的写法行不通,OnSendAsync 需要被重写。DelegatingHandler 没有提供一个直接接受委托的重载。但你可以自己包装一层:

public sealed class ActionHandler : DelegatingHandler { private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _action; public ActionHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> action) { _action = action; } protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return _action(request, cancellationToken); } }

我确实见过有团队在项目里封装了这个ActionHandler用来临时添加一些简单逻辑,比如只对某个端点生效的特殊Header、或者简单的请求改写。但它不适合做复杂横切逻辑,因为可读性会变差,建议控制使用频率。

4.2 Handler的责任链模式与其他模式的结合

DelegatingHandler 本身就是一个责任链模式的范例。你完全可以用它实现类似装饰器的能力,比如给请求响应做加解密。

假设你对接了一个需要AES加密请求体的第三方接口。你可以把加解密逻辑封装成两个Handler:

public class EncryptHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Content != null) { var raw = await request.Content.ReadAsStringAsync(cancellationToken); var cipher = Encrypt(raw); request.Content = new StringContent(cipher, Encoding.UTF8, "text/plain"); } return await base.SendAsync(request, cancellationToken); } } public class DecryptHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); if (response.Content != null) { var raw = await response.Content.ReadAsStringAsync(cancellationToken); var plain = Decrypt(raw); response.Content = new StringContent(plain, Encoding.UTF8, "application/json"); } return response; } }

注意这两个Handler必须分别放在链路的最外层和最内层,因为加密要在所有Header处理完之后、最接近网络时执行,解密则要在最接近网络时执行。如果顺序乱了,日志层看到的可能还是加密数据,排查问题时会一头雾水。

4.3 通过Handler实现A/B Testing流量染色

稍微进阶一点的用法是流量染色。你在网关或下游服务里想要区分测试流量和正式流量,可以在Handler里给请求增加一个特殊的Header:

public class StickyHandler : DelegatingHandler { private readonly string _environment; public StickyHandler(string environment) { _environment = environment; } protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { request.Headers.Add("X-Env-Tag", _environment); return base.SendAsync(request, cancellationToken); } }

这种做法在做灰度发布、A/B实验、压测标记场景中非常实用。只需要在注册不同HttpClient客户端时使用不同的Handler配置,就能让同一条调用链发出携带不同标记的请求,下游配合拦截逻辑即可实现分流。我在一个项目里用这个方式给后台服务做的流量染色,压测时用染色流量打标,生产环境再用另一组无染色逻辑的客户端,整体隔离效果很好。

4.4 用Handler实现缓存短路

如果你有某些接口的响应短期内不会变化,可以在Handler里加一层内存缓存。例如,在 SendAsync 里先查内存缓存,命中就直接返回 CachedResponse,完全不调用 base.SendAsync;未命中则发请求并写入缓存。因为这个逻辑和业务完全解耦,你随时可以把这个Handler摘掉而不影响业务。

public class CacheHandler : DelegatingHandler { private static readonly ConcurrentDictionary<string, HttpResponseMessage> Cache = new(); protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var key = $"{request.Method}:{request.RequestUri}"; if (request.Method == HttpMethod.Get && Cache.TryGetValue(key, out var cached)) { return new HttpResponseMessage(cached.StatusCode) { Content = new StringContent(cached.Content.ReadAsStringAsync().GetAwaiter().GetResult(), System.Text.Encoding.UTF8, "application/json") }; } var response = await base.SendAsync(request, cancellationToken); if (request.Method == HttpMethod.Get && response.IsSuccessStatusCode) { Cache[key] = response; } return response; } }

注意我这里用了 GetAwaiter().GetResult(),是为了在完整代码里演示,实际上在同步方法里做异步操作是ASP.NET Core的死锁陷阱之一。如果你要用缓存Handler,建议把缓存内容转成字节数组或字符串存储,避免持有HttpResponseMessage引用导致连接无法释放。缓存逻辑本身并不复杂,但你要考虑过期策略、缓存大小、并发访问,这些细节决定了它能不能安全上线。

5. 避坑指南:Handler使用中的常见问题和排查技巧

5.1 “The inner handler has not been assigned”异常

这个异常的原因是某个Handler的InnerHandler为null,到了SendAsync要往下传时找不到下一层。在手动链式装配时特别容易出现,比如你只写了 new RetryHandler() 却忘了指定InnerHandler。

排查方式很简单:检查每个Handler对象是否都正确设置了InnerHandler,并且链的末端是HttpClientHandler或SocketsHttpHandler。HttpClientHandler本身没有InnerHandler,它就是处理TCP连接的那个终端处理器。

在DI容器中注册时,如果你用 AddHttpMessageHandler 扩展方法注册Handler,IHttpClientFactory 会自动把链的末端设置为内部 Handler,不需要你手动处理InnerHandler。这也是为什么我更推荐用IHttpClientFactory的原因之一,这个坑它帮你填了。

5.2 响应体被提前消费的问题

日志Handler里读了响应体,业务代码再读就返回空字符串,这类问题非常隐蔽。原因是对流的一次性读取。

解决方案一般有三个:一是不要读响应体,只记录状态码和耗时,需要body的单独在业务层记录;二是重新包装Content,把已经读取的字符串重新赋给 response.Content;三是启用可缓冲的响应处理,在Handler里把流复制一份再保留到内存。我在生产环境直接用第二种方案,简单可靠。

5.3 Handler实例的生命周期与状态管理

在 IHttpClientFactory 模式下,注册的 Handler 默认是按需创建的,你可以把 Handler 注册成不同生命周期。但如果注册成 Singleton,Handler 内部一旦存在可变状态,很容易出现并发问题。

比如你在 Handler 里放了一个 int 字段用来计数,多个请求同时进来,这个字段就会被并发修改。哪怕你用了锁,整体的吞吐也会被拖垮。Handler 内部最好是无状态的,或者只持有线程安全的依赖。如果你真的需要状态管理,至少也应该是 AsyncLocal 或 ConcurrentDictionary 这种线程安全结构。

5.4 在每个请求上都创建HttpClient的问题

虽然这和DelegatingHandler没有直接关系,但每次 new HttpClient 导致的socket耗尽问题是很多Team遇到的典型故障。HttpClient 是轻量级对象,但底层Socket连接不是。频繁创建会在TIME_WAIT状态堆积大量连接,最终端口耗尽。

这个问题的根因是 HttpClient 默认使用旧的Handler实例,不会复用连接。用 IHttpClientFactory 之后,Handler 池帮你管理了连接生命周期,问题就迎刃而解。所以对于任何新项目,我都建议直接上 IHttpClientFactory + DelegatingHandler 这套组合。

5.5 在Handler里用了同步等待导致死锁

这就是那句大家常说的“sync over async”。如果你在Handler里写 response.Content.ReadAsStringAsync().Result,在AspnetCore默认同步上下文里很容易出问题。

正确做法是所有IO操作全部用 await,避免任何形式的同步等待。不仅Handler里如此,业务层调用HttpClient时也尽量用 async/await,这样既能避免死锁,也能提升线程利用率。

5.6 把CancellationToken传递丢了

很多Handler实现里容易漏掉把 cancellationToken 传给 base.SendAsync。如果你漏了,用户取消操作时请求不会中止,浪费了资源和时间。

另外一个重要点是Task.Delay、ReadAsStringAsync 这类方法也尽量传入 cancellationToken,否则即使请求取消,你还在等待延迟或读取流。重试Handler里尤其要注意,否则一个取消操作会让你的后台线程继续运行很久。

6. 实战问答速查:常见问题与解决方案速查表

现象排查思路解决方案
请求发送两次重试Handler里没有克隆请求,直接发送了同一个HttpRequestMessage每次重试前克隆请求
日志里看不到响应体某个内层Handler提前消费了流日志Handler放在最外层或重新包装Content
下游收到重复数据对POST接口做了盲目重试仅对幂等请求重试,或加入幂等键
Token总是旧的Handler里直接读了一次Token就不再更新TokenProvider内部做缓存刷新机制
超时时间不准超时Handler放在内层,计时不包含外层耗时超时/日志Handler放在最外层
特定请求不想走某些逻辑在Handler里根据URL或Header做条件判断通过请求属性传递标记,Handler只处理带标记的请求
HttpClient耗尽socket每次new HttpClient改用IHttpClientFactory + Handler池
取消操作无效没有把CancellationToken传给base.SendAsync或Task.Delay所有异步方法统一传入cancellationToken
并发下Handler状态错乱Handler里有可变实例字段改为无状态设计或使用线程安全容器

7. 我的一些额外实操建议

7.1 先用好默认参数,再考虑自定义

很多需求可以用 HttpClient 自带的超时、默认Header、默认请求版本搞定,不要什么事都上Handler。Handler是为横切逻辑设计的,不是万能的。过度设计会让调用链变得难以理解和维护。

我见过一个项目,在一条链上挂了七个Handler,每个Handler就加一个Header,职责高度分散,排查一次问题要翻七份代码。如果是加Header这种简单需求,在HttpClient默认请求头里配置一条就足够了。

7.2 选择合适的Handler粒度

Handler的粒度取决于你的使用场景。你可以按功能拆分成日志、认证、重试这种通用型Handler,也可以按具体业务拆分成某第三方API专属的签名Handler、加解密Handler。我的建议是:通用功能做全局Handler,业务特性做按客户端注册的Handler,不要混在一起。如果两个模块对同一个接口的处理逻辑不同,就分别注册不同的HttpClient,各自独立配置Handler链。

7.3 链路追踪和异常处理的最佳实践

在微服务架构里,DelegatingHandler 是实现链路追踪Header传播的理想位置。你可以在Handler里统一生成或透传 TraceId、SpanId,随请求发送到下游服务。异常处理也是一样,可以在最外层的Handler里统一捕获异常并包装为自定义响应。但要注意:在最外层捕获异常后,你就丢失了下游的具体错误信息,建议在抛给业务层之前至少记全日志。

7.4 性能监控:把Handler变成天然的Metrics采集点

我之前在一次压测中发现,性能数据缺失会严重影响容量评估。后来我在最外层加了一个简单的MetricsHandler,每次请求都记录耗时直方图、成功失败计数、QPS。无需改动业务代码,就能在监控系统里看到每个下游接口的耗时表现。这对定位慢调用、合理设置超时重试参数非常有效。如果你已经在用Prometheus之类的监控体系,把Handler作为Metrics采集点是成本最低的方案。

8. 最后:从能用走向好用

回看我最早做的那次超时排查,如果我当初就用 DelegatingHandler 把日志、认证、重试全部收拢,可能根本不用翻那么久的代码。这个设计模式并不高深,核心还是对横切关注点的合理组织。你先从日志Handler写起,加上认证,再加上重试,逐步积累,就会越来越体会到这条“消息管道”的灵活之处。

我个人在实际项目中最喜欢的一个做法是:把所有业务无关的处理器放在独立项目中,做成一个可复用的类库。不管后续是 Web API 项目、后台任务,还是单元测试的Mock客户端,都可以通过注册这套Handler获得一致的日志、认证和可靠性能力。代码写完之后,维护成本极低,排查问题时链路清晰,各层职责一目了然。

如果你刚开始接触,不用急着把所有玩法都上,先写一个日志Handler,跑一次请求看看效果,再逐步增加其他逻辑。等你熟悉了整个链路的流转机制,会发现DelegatingHandler能帮你做的不只是清理代码——更是一套可复用的架构基建。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:50:50

智慧蜂业无人机集群协同测试体系:架构、指标与故障复盘

第一次在蜂场试飞三机编队&#xff0c;原本排队出勤的蜜蜂像被捅了马蜂窝一样炸开了。那一刻我意识到&#xff0c;智慧蜂业无人机集群协同测试&#xff0c;绝对不是在田里飞一圈看它稳不稳那么简单。我们后来花了大半年&#xff0c;围绕一个智慧蜂业示范项目&#xff0c;把集群…

作者头像 李华
网站建设 2026/10/17 5:50:03

计算机组成原理中的MIPS:指令格式、数据通路与流水线入门

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:48:34

俄罗斯方块模拟题从40分到高分:核心难点与代码框架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:48:33

流水作业调度问题详解:Johnson法则、代码实现与排产应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:48:07

单细胞通讯分析拆解肿瘤转移机制:从注释到配体受体推断

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:46:06

从慢查询到SQL优化:索引、执行计划与实战经验全解析

半夜两点钟手机震个不停&#xff0c;某电商订单系统的监控群连续弹了三页告警&#xff0c;核心查询接口的响应时间从平时的200毫秒直接飙到了4秒开外。第一次排查慢查询的人可能手忙脚乱&#xff0c;但经验多了你会发现&#xff0c;大部分SQL性能问题翻来覆去就那么几类&#x…

作者头像 李华