零、 核心抉择:什么时候用中间件?什么时候用依赖注入 (Depends)?
很多初学者容易把这两个拦截机制搞混。其实区分它们的标准非常简单直接:
-
中间件 (Middleware) ------ "一网打尽"的全局保安
- 何时使用 :当你需要对所有的请求(不论是查书还是登录)进行统一处理时用它。比如:配置全局的跨域 CORS 放行、统计所有接口的请求耗时、记录所有访问者的全局日志。
- 致命局限 :它站在系统最外围(洋葱壳最外面),只能看到最原始的 HTTP 报文(URL、Header)。它不知道 这个请求最终会匹配到哪个具体的路由,也拿不到前端传来的具体 JSON 内部字段。
-
依赖注入 (Depends) ------ "按需发配"的专属助理
- 何时使用 :当你只需要对特定的一部分路由 进行拦截或提供工具时用它。比如:只有"修改密码"接口需要校验 Token,而"查看新闻"接口不需要;或者只给需要查库的接口发放数据库
session。 - 核心优势:它和路由函数绑定得非常深。它不仅知道当前访问的是哪个路由,还能像路由一样,精准提取和验证路径参数、甚至是 Body 里的具体某个字段。
- 何时使用 :当你只需要对特定的一部分路由 进行拦截或提供工具时用它。比如:只有"修改密码"接口需要校验 Token,而"查看新闻"接口不需要;或者只给需要查库的接口发放数据库
-
处理层级与灵活度
- 中间件 (系统级别) :它主要作用于底层的 HTTP 协议/系统层面。它比较"死板",面对的是最原始的 Request 对象,因此它最适合用来干底层网络层面的脏活(改原生 Header、处理跨域机制)。
- 依赖注入 (任意业务级别) :它完全是业务级的,你可以毫无限制地"随便弄"。只要你能写出一个 Python 函数(无论里面是计算积分、生成验证码、还是连接数据库),你就可以随便把它当成一个依赖注入进来,极度灵活自由。
一、 依赖注入的核心概念与设计优势
- 一句话结论:依赖注入就是将共享的通用逻辑抽离为可重用组件(函数或类),由 FastAPI 框架自动调用并将结果注入给路由函数。
1. 核心名词拆解
- 依赖项 (Dependency):一个可重用的组件(函数或类),负责提供某种特定功能、数据计算或资源准备。
- 注入 (Injection):FastAPI 框架在执行目标路由函数前,自动调用依赖项函数,并将计算结果作为形参赋值给路由函数。
2. 三大核心架构优势
- 代码复用(DRY 原则):一次编写,多处路由无缝共享,彻底消除重复代码。
- 业务逻辑解耦:将基础设施代码(如 Auth 鉴权、DB 会话管理)与业务视图逻辑解耦,保持路由函数纯粹。
- 极易单元测试 (Easy Testing) :在自动化测试中,可通过依赖覆盖(
app.dependency_overrides)轻松将真实数据库依赖替换为 Mock 模拟数据。
二、 依赖注入的四大经典应用场景
text
请求到达 ➡️ [依赖项 1: 参数提取] ➡️ [依赖项 2: 安全认证] ➡️ [依赖项 3: DB Session 创建] ➡️ 执行路由逻辑 ➡️ 自动回收 DB 连接
1. 提取与验证请求参数(多参数自由混合机制)
从请求中自动统一提取并校验复杂参数,支持与路径参数 (Path)、查询参数 (Query) 自由混合编写:
python
from fastapi import FastAPI, Depends, Header, Path, Query
app = FastAPI()
# 依赖项:统一提取与校验 HTTP 请求头
def get_request_headers(
user_agent: str = Header(...), # 必填请求头:必须携带 User-Agent
x_token: str | None = Header(None) # 选填请求头:可选携带 X-Token,默认 None
):
return {"user_agent": user_agent, "has_token": bool(x_token)}
# 路由函数:路径参数 + 查询参数 + 依赖注入参数 自由混合(逗号分隔并列)
@app.get("/info/{category}")
def get_info(
category: str = Path(..., description="分类名称(路径参数)"),
user_id: int = Query(..., description="用户ID(查询参数)"),
detail: bool = Query(default=False, description="是否返回详情(查询参数)"),
headers: dict = Depends(get_request_headers) # 依赖注入参数
):
return {
"category": category,
"user_id": user_id,
"detail": detail,
"headers": headers
}
【HTTP 客户端请求报文示例】:
httpGET /info/tech?user_id=1001&detail=true HTTP/1.1 Host: 127.0.0.1:8000 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X) X-Token: secret-999响应结果 JSON:
json{ "category": "tech", "user_id": 1001, "detail": true, "headers": { "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X)", "has_token": true } }
【代码深度解析与核心机制】:
- 多参数自由混合机制 : 在路由函数的参数括号中,
Path路径参数、Query查询参数和Depends依赖注入参数完全使用逗号,分隔并列书写。FastAPI 会自动识别参数声明来源并完成多路分发赋值。- 为什么用 Header() 而非普通类型声明? 若直接写
user_agent: str,FastAPI 会默认作为 URL 查询参数(Query)处理;使用Header()明确指定从 HTTP 请求头(Request Headers)中提取。- 三个点
...(Ellipsis) 的含义 : 在 FastAPI 与 Pydantic 中,...作为默认值代表 "该字段为必填项 (Required)" (与 Pydantic 的Field(...)语义完全一致)。若客户端未传递必填项,FastAPI 会自动拦截并返回422 Unprocessable Entity错误。Header(None)的含义 : 显式指定默认值为None,代表该请求头为 "选填项 (Optional)" ,客户端未传时自动赋值为None。- 命名自动映射(下划线 ➡️ 短横线) : HTTP 标准规范使用短横线命名(如
User-Agent、X-Token),而 Python 变量使用下划线(如user_agent、x_token)。FastAPI 会自动将 Python 的下划线映射为 Header 的短横线进行匹配读取,无需手动做字符串转换。
2. 安全与身份认证 (Security & Auth)
验证用户 Token、提取当前登录用户,并校验角色权限(权限不通过直接抛出 HTTPException 阻断):
python
from fastapi import FastAPI, Depends, HTTPException, status
# 依赖项:用户 Token 校验切面
def get_current_user(token: str):
if token != "valid-token":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未提供有效凭证")
return {"user_id": 42, "role": "admin"}
@app.get("/dashboard")
def get_dashboard(user: dict = Depends(get_current_user)):
return {"message": f"欢迎回来,用户 {user['user_id']}"}
3. 共享数据库连接与生命周期管理 (Yield Dependencies)
管理数据库 Session 会话的创建、挂起使用与自动关闭销毁:
python
# 依赖项:生成器依赖项控制资源生命周期
def get_db():
db = create_db_session() # 1. 建立 DB 连接
try:
yield db # 2. 注入给路由使用
finally:
db.close() # 3. 响应完成后自动关闭连接,防止连接池泄漏
4. 共享通用业务逻辑 (Shared Business Logic)
抽离封装多个路由公用的复用算法、第三方服务客户端初始化等:
python
# 依赖项:通用分页器算法
class Paginator:
def __init__(self, page: int = 1, size: int = 20):
self.skip = (page - 1) * size
self.limit = size
@app.get("/posts")
def list_posts(pagination: Paginator = Depends()):
return {"skip": pagination.skip, "limit": pagination.limit}
三、 洋葱模型与 yield 底层执行流解密(硬核剖析)
在 FastAPI 的依赖注入中,依赖项分为普通函数 与生成器函数 (带 yield) 两种,它们的底层执行流截然不同:
1. 普通依赖函数(没有 yield,只有 return)
用于纯粹的计算、参数提取或权限校验(如鉴权、解密 Token 等)。
-
底层执行流 :单向直达,没有回头路。
python# FastAPI 底层调度伪代码: try: # 1. 路由执行前,一次性将依赖函数执行完毕 token = verify_token() # 2. 将结果塞给路由函数,执行核心业务 执行_你的路由函数(token=token) except Exception as e: # 3. 如果报错,直接由 FastAPI 顶层全局拦截返回响应,不会再回传给依赖函数 交给全局异常处理器()
2. 生成器依赖函数(带有 yield)
专用于需要双向生命周期管理的场景,如数据库连接、文件句柄(即用完必须关闭、发生异常必须回滚的资源)。
-
yield 的本质 :
yield是 Python 的原生语法,作用是**"交出变量,并让当前函数在这一行冻结(暂停)"**。FastAPI 巧妙利用这个特性创造了"洋葱包裹模型"。 -
底层执行流 :支持"暂停、唤醒、与异常回抛"。
python# FastAPI 底层调度伪代码: gen = get_db() # 1. 初始化生成器 session = next(gen) # 2. get_db 往下跑,直到遇到 yield,交出 session 并在 yield 处冻结暂停 try: 执行_你的路由函数(db=session) # 3. 路由拿到 session 开始执行具体的增删改查 next(gen) # 4. 路由成功执行完,唤醒 get_db,让它继续执行 yield 下半部分(如 commit 和 close) except Exception as e: gen.throw(e) # 5. [核心魔法]:如果路由报错,FastAPI 会主动把异常硬生生砸回给 yield 处!从而触发 get_db 里的 except 执行 rollback 回滚 -
总结 :
yield将原本一竿子插到底的代码劈成了两半,上半场负责分配资源 ,下半场被唤醒后负责回收兜底 。这让你在写具体的路由业务时,完全不用再操心commit或rollback,实现了极致的代码解耦!
四、 【核心细节与避坑指南】
- 细节 1(依赖项注入缓存机制) :在一个请求中,如果多个路由或子依赖项依赖了同一个函数,FastAPI 默认只执行该依赖项一次并缓存结果。若需每次强制重算,可设置
Depends(func, use_cache=False)。 - 细节 2(支持注入多个不同依赖) :一个路由函数可以同时注入多个不同的依赖项(如
def handler(db = Depends(get_db), user = Depends(get_current_user))),FastAPI 会按顺序自动执行全部依赖项。 - 细节 3(Header 与 Field 的三个点一致性) :FastAPI 所有参数声明类(
Header、Query、Path、Cookie、Field)均遵循同一规则------传入...代表必填,传入具体值或None代表选填并赋予默认值。
五、 【终极一句话速记】
"公共逻辑抽依赖,Depends 注入解耦合;多参混合逗号隔,Yield 闭环管资源。"