FastAPI系列-11-中间件与依赖注入的区别

FastAPI系列-11-中间件与依赖注入的区别

_

11 中间件与依赖注入的区别 —— 同一个需求,放中间件还是放依赖?

前置阅读: 09-中间件10-依赖注入
关键词: 对比, 选型, 触发时机, 作用粒度
难度: ★★★☆☆

场景导入:鉴别一个横切关注点该去哪

前面两章分别讲了中间件和依赖注入。它们都能让图书 API 复用横切逻辑——在请求处理链的某个环节插入一段公共代码。但它们的触发时机、可见范围和适用场景截然不同。

一个常见的翻车现场是:把鉴权逻辑写进了中间件,结果连 /health 健康检查接口都被要求带 token——Kubernetes 的 liveness probe 直接挂掉。另一个翻车现场是:把访问日志写进了依赖注入,结果 404 和异常响应在日志里一片空白——因为这些路径根本没走到依赖解析那一步。

本篇用一张全维度对比表把两种机制的区别讲清楚,并给出三条选型原则。读完你能立刻判断:一个新需求,应该挂到中间件链上,还是注册为依赖。

原理解析:触发时机决定了一切

中间件和依赖注入在请求处理链上的位置不同——这是所有区别的根源。

sequenceDiagram participant C as 客户端 participant M as 中间件链 participant R as 路由层 participant D as 依赖解析 C->>M: 发送请求 M->>R: 进入路由匹配 R->>D: 解析端点依赖 D-->>R: 返回注入参数 R-->>M: 生成响应 M-->>C: 返回响应

(图注:请求先经过中间件链,路由匹配成功后才进入依赖解析;响应沿相反方向返回。)

关键的时间差在于:中间件在路由匹配之前就已经在执行了。这意味着即使客户端请求了一个不存在的路径(/nonexistent),中间件仍然会触发;而依赖注入只在路由匹配成功后、handler 执行前才解析。

用一张流程图可以更清楚地看到两者的"势力范围":

flowchart LR A["请求"] --> B["中间件链"] B --> C{"路由匹配"} C -- "否" --> D["404 响应"] C -- "是" --> E["依赖解析"] E --> F["handler"] F --> G["响应生成"] G --> H["中间件链"] H --> I["客户端"] subgraph 中间件作用范围 A B D G H I end subgraph 依赖注入作用范围 E F end

(图注:中间件覆盖整个 ASGI 调用栈(包括 404 和异常响应),依赖注入只覆盖路由匹配之后的部分。)

全维度对比

下表覆盖了实际选型需要考虑的每个维度:

维度

中间件

依赖注入

触发时机

请求进入路由前 + 响应返回后,覆盖整个 ASGI 调用栈

路由匹配成功后,handler 执行前解析

作用对象

RequestResponse 对象

handler 形参(通过 Depends 或类型注解)

可访问上下文

ASGI scope、请求头、请求体、响应头、状态码

handler 签名声明的依赖,可在依赖内访问 Request

可见范围

所有请求,包括 404、异常响应

仅匹配成功的路由端点

典型用途

访问日志、请求 ID、响应压缩、CORS、全站鉴权

分页参数、当前用户、数据库会话、特性开关

性能影响

每条请求穿过全部中间件,链长放大调用栈

只解析 handler 实际声明的依赖,子依赖缓存复用

测试便利性

需要构造完整 ASGI 请求

可通过 dependency_overrides 替换,更易单元测试

文档可见性

不直接出现在 OpenAPI schema 中

Depends 与类型注解会被 OpenAPI 识别

配置粒度

应用级或子应用级,粒度较粗

单个端点或单个形参,粒度精细

三条选型原则

从这张表可以归纳出三条原则,日常开发中几乎能覆盖所有场景:

原则一:协议级需求走中间件。 任何与 HTTP 协议本身相关的横切关注点——响应头注入、CORS、压缩、访问日志——都应该放中间件。因为它们对所有 ASGI 请求生效,与具体端点无关,即使路由匹配失败也需要记录和响应。

原则二:业务级需求走依赖。 分页、当前用户、数据库会话等只在部分端点需要的对象,应该放依赖。这样不需要这些对象的端点(如 /health)就不会被拖入解析链,也不用在中间件层做"哪些路径需要鉴权"的白名单维护。

原则三:跨层数据通过 request.scope 传递。 请求 ID、追踪标识等需要贯穿全链路的字段,先在中间件写入 request.scoperequest.state,再由依赖按需读取并附加到日志或业务字段。这样中间件保持"无业务知识",依赖保持"无协议知识",各管各的。

下面用一个具体的代码示例展示依赖注入在路由中的常见写法——分页、当前用户、数据库会话三类依赖同时出现在一个 handler 签名中:

# app/api/books.py
from typing import Annotated

from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

from app.db import get_db


class Pagination(BaseModel):
    page: int = Field(1, ge=1)
    size: int = Field(20, ge=1, le=100)


def get_pagination(
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
) -> Pagination:
    """分页依赖:从查询参数构造 Pagination 对象。"""
    return Pagination(page=page, size=size)


def get_current_user() -> dict[str, object]:
    """当前用户依赖的最小定义,生产环境应校验令牌。"""
    return {"name": "demo"}


router = APIRouter(prefix="/books", tags=["books"])

PaginationDep = Annotated[Pagination, Depends(get_pagination)]
UserDep = Annotated[dict[str, object], Depends(get_current_user)]
DBDep = Annotated[Session, Depends(get_db)]


@router.get("")
async def list_books(
    pagination: PaginationDep,
    user: UserDep,
    db: DBDep,
) -> dict[str, object]:
    return {
        "page": pagination.page,
        "size": pagination.size,
        "user": user["name"],
        "items": [],
    }

list_books 同时声明了分页、用户和会话三类依赖。如果请求路径是 /unknown,中间件仍然记录访问日志,但 get_paginationget_current_userget_db 都不会被调用——路由匹配失败的请求根本走不到依赖解析。这就是"可见范围"差异的最直观体现。

避坑指南

  • 不要把同样的逻辑注册两次。鉴权拆成"中间件做全站拦截 + 依赖做端点级权限校验"是合理的,但把同一个 token 解析在中间件和依赖里各做一遍就是浪费。明确分工:中间件负责全局放行检查,依赖负责"当前用户"的具体权限。

  • 中间件改写响应体后,response_model 可能再次校验。如果中间件改了响应体结构,而路由层声明了 response_model,框架会按模型再次校验——可能产生双序列化或不匹配。建议中间件只改写响应头,响应体由 handler 和 response_model 统一控制。

  • 分散注册会让顺序和可见范围难以推断。中间件链和依赖图都应在 main.pydependencies.py 中集中描述并附注释。当项目中出现第五个中间件和第十个依赖时,集中管理的好处会非常明显。

面试 QA

Q1 [原理]: 中间件与依赖注入的本质区别是什么?

中间件作用于 ASGI 调用栈的请求-响应全链路,横切所有路由与异常响应;依赖注入作用于 handler 的形参,只在路由匹配成功并进入 handler 时才执行。

展开来看:中间件在请求进入路由之前与响应离开路由之后生效,可以观察和改写请求体、响应头、响应体与异常响应;依赖注入由 solve_dependencies 在请求期按拓扑序解析,只能影响 handler 及其子依赖。中间件的设计前提是"对所有请求生效",适合协议级横切;依赖注入的设计前提是"对部分端点生效",适合业务级复用。

源码层面,中间件栈由 FastAPI.build_middleware_stack 在启动期拼装,依赖图由 fastapi/dependencies/utils.pysolve_dependencies 在请求期解析。两者由不同模块负责,但共享 request.scope 作为上下文传递通道。

Q2 [项目]: 图书 API 中哪些横切关注点应该放入中间件,哪些应该放入依赖?

协议级和全站级 → 中间件。业务级和按端点可控的 → 依赖。两者通过 request.scope 协同。

具体来说:访问日志、请求 ID、响应压缩、CORS、安全响应头——这些都是"对所有 ASGI 请求生效"的协议级关注点,放中间件可以一次性覆盖路由、异常处理器和 404 路径。当前用户、数据库会话、分页参数、特性开关——这些都是"只在特定端点生效"的业务级关注点,放依赖可以让 /health 这类端点完全跳过相关解析。

协同方式:中间件写入 request.state.request_id,依赖读取它并附加到业务日志。鉴权可以拆为两步——中间件完成全局 token 解析和放行检查,依赖完成端点级权限校验。日志指标也应区分两层:中间件层关注状态码、耗时和请求 ID,依赖层关注业务字段——两端结合构成完整的请求画像。

小结

本篇用一张全维度对比表 + 三条选型原则,把中间件和依赖注入的边界画清楚了。记住一句话就够了:协议级走中间件,业务级走依赖。两者不是竞争关系,而是协作关系——中间件提供请求级维度(日志、计时、ID),依赖提供业务级维度(用户、会话、分页),通过 request.scope 传递上下文。

下一篇《12 ORM 介绍》将进入数据层,从关系型数据库的基础概念开始,讨论 FastAPI 项目中的数据持久化思路——原生 SQL 和 ORM 各自的适用场景是什么。

FastAPI系列-12-ORM介绍 2026-06-24
FastAPI系列-10-依赖注入 2026-06-23

评论区