StopWatch是Spring框架提供的高精度计时工具,基于性能计数器,精度达微秒级,支持分段计时、暂停继续、格式化输出等功能,适用于接口监控、任务调度和日志记录等场景。

直接用 Stopwatch + 中间件是最轻量、最可控的方案,不需要引入 AOP 框架或第三方库,也避开了 DateTime.Now 的精度和时钟漂移问题。
为什么不用 DateTime.Now 记录接口耗时
DateTime.Now 分辨率低(通常 10–15ms),在短请求(如几毫秒的 GET)中误差显著;且系统时钟可能被 NTP 调整,导致 ElapsedMilliseconds 出现负值或跳变。而 Stopwatch 基于高精度性能计数器(QueryPerformanceCounter),.NET 中默认启用,精度可达微秒级。
常见错误现象:ElapsedMilliseconds 为负数、同一请求两次日志差值异常大、耗时统计明显偏离实际(比如显示 2ms,但 Fiddler 抓包是 18ms)。
- 必须用
Stopwatch.Start()和Stopwatch.Stop()配对,不能只靠Start()后直接读ElapsedMilliseconds - 避免在
await next()前后用两个独立的Stopwatch实例——上下文可能跨线程,计时会断裂 - 不要在中间件里调用
Stopwatch.Restart(),它不保证原子性,高并发下易出错
中间件中正确使用 Stopwatch 的写法
关键在于把 Stopwatch 实例绑定到当前 HttpContext 生命周期,确保「开始→结束」严格对应一次请求处理链。
实操建议:
- 在
async (context, next) => { ... }作用域内创建并启动Stopwatch,不要提前提取为字段或静态变量 - 务必在
await next()之后再调用Stop(),否则计时不包含下游中间件和控制器执行时间 - 用
context.TraceIdentifier关联日志,便于和分布式追踪对齐 - 日志输出字段建议固定为:
RequestMethod、RequestPath、StatusCode、ElapsedMilliseconds,方便后续按字段聚合分析
示例片段:
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
await next();
sw.Stop();
<pre class="brush:php;toolbar:false;">var logger = context.RequestServices.GetRequiredService<ilogger>>();
logger.LogInformation(
"REQ {TraceId} {Method} {Path} - {StatusCode} in {ElapsedMs}ms",
context.TraceIdentifier,
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);</ilogger>});
和 ActionFilter 方案对比:什么时候该选中间件
中间件记录的是整个 HTTP 请求生命周期(含静态文件、认证、授权、MVC 路由前处理等),而 RequestTimeActionFilter 只覆盖 MVC 控制器的 Action 执行阶段。如果你要定位「慢在路由?慢在模型绑定?还是慢在业务逻辑?」,必须用中间件。
容易踩的坑:
- 把
UsePerformanceLog()放在UseRouting()之前 → 无法获取RequestPath,Path为空 - 放在
UseEndpoints()之后 → 日志里StatusCode总是 200,因为响应已提交,状态码不可读 - 未检查
context.Response.HasStarted就读StatusCode→ 可能抛InvalidOperationException
推荐顺序:
app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UsePerformanceLog(); // 放这里 app.UseEndpoints(...);
生产环境要注意的细节
中间件本身不能成为性能瓶颈。高频日志(如每秒数千请求)直接打 ILogger.LogInformation 会拖慢吞吐量。
优化建议:
- 对
ElapsedMilliseconds 的请求,默认不记日志,或降级为 <code>LogDebug - 用结构化日志框架(如 Serilog)时,加
LogEventLevel.Warning级别阈值:耗时 > 1s 才触发LogWarning - 避免在日志消息里拼接字符串(如
"耗时" + ms + "ms"),改用占位符,减少 GC 压力 - 如果需导出指标(如 Prometheus),不要在中间件里直接暴露
/metrics,应走专门的IMetricsRoot或Counter类型度量
真正容易被忽略的一点:中间件记录的是「服务端处理耗时」,不含网络传输、DNS、TLS 握手、客户端渲染等环节。要完整看端到端性能,得和前端 Navigation Timing API 或 APIM 网关日志对齐,不能只盯着这一层。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











