FastAPI 系列 · 第 3 篇:依赖注入------用 Depends 构建分层架构
适合人群 :熟悉 Java Spring Boot、已完成第 01、02 篇的后端工程师
阅读时间 :约 30 分钟
一句话定位 :FastAPI 的Depends()是请求级的依赖解析引擎,本篇以shop-api为载体,完整实现 Router → Service → Repository 三层架构,并建立与 Spring IoC 容器的认知映射。
一、Depends 机制原理
Spring Boot 工程师对依赖注入(Dependency Injection,DI)并不陌生------Spring IoC 容器在应用启动时扫描 @Component、@Service、@Repository,将 Bean 装入容器,在需要时自动注入。FastAPI 的 Depends() 解决的是同一个问题,但采用了截然不同的实现路径:请求级函数解析,而非容器级对象管理。
1.1 Depends() 工作原理
当 FastAPI 接收到一个 HTTP 请求时,它并非直接调用路由函数,而是先遍历函数签名中所有带有 Depends() 标记的参数,递归地解析整棵依赖图(Dependency Graph),然后将解析结果注入路由函数并执行。
python
from fastapi import FastAPI, Depends
app = FastAPI()
# 这是一个"依赖函数",就是普通的 Python 函数
def get_query_token(token: str = "default_token") -> str:
return token
# 路由函数声明依赖
@app.get("/items")
async def read_items(token: str = Depends(get_query_token)):
# FastAPI 会先调用 get_query_token(),把返回值赋给 token
return {"token": token}
这里有几个关键点值得注意:
Depends(get_query_token)传入的是函数本身(不带括号),FastAPI 负责在正确的时机调用它- 依赖函数的参数同样遵循 FastAPI 的参数解析规则------
token: str会被识别为查询参数 - 路由函数只声明"我需要什么",不关心"如何获取"------这正是依赖倒置原则(DIP)的体现
1.2 完整请求处理序列图
路由函数 get_db get_product_service Depends 解析器 FastAPI Router 客户端 路由函数 get_db get_product_service Depends 解析器 FastAPI Router 客户端 GET /products/42 解析路由函数签名 发现 Depends(get_product_service) 发现 Depends(get_db) 返回 AsyncSession 返回 ProductService(db) 注入所有依赖 返回 ProductResponse 200 OK + JSON
这个序列图揭示了两个重要特性:
- 依赖是有序解析的 :FastAPI 会先解析最底层的依赖(
get_db),再逐层向上构建(get_product_service),最后才调用路由函数 - 依赖可以嵌套任意深度:A 依赖 B,B 依赖 C,C 依赖 D------FastAPI 会递归展开整棵依赖树
1.3 与 Spring IoC 的类比对照
| 维度 | Spring IoC | FastAPI Depends |
|---|---|---|
| 生命周期 | 应用级(Singleton/Prototype/Request) | 请求级(每次请求重新解析) |
| 注入方式 | 构造器注入 / 字段注入 / Setter 注入 | 函数参数声明 |
| Bean 注册 | @Component、@Bean、XML 配置 |
直接传入 callable,无需注册 |
| 循环依赖 | 框架自动检测并处理(部分场景报错) | 运行时图解析,天然避免构造器循环依赖 |
| 测试替换 | @MockBean、@TestConfiguration |
app.dependency_overrides |
| 配置驱动 | @Value、@ConfigurationProperties |
依赖函数直接读取环境变量 |
🤔 核心思维差异:Spring IoC 的哲学是"容器持有对象,对象长期存在";FastAPI 的哲学是"每次请求重新构建依赖链,用完即丢"。前者适合有状态的单例 Bean,后者天然适合无状态的 HTTP 请求处理。
1.4 依赖函数的多种形态
FastAPI 对依赖函数的类型没有限制,任何 callable 均可作为依赖。
python
import time
from fastapi import Depends, Request
# 形态一:普通函数(同步)
def get_settings():
return {"debug": True, "version": "1.0"}
# 形态二:异步函数(推荐用于 I/O 操作)
async def get_db_async():
async with AsyncSessionLocal() as session:
yield session
# 形态三:类(__init__ 作为依赖函数)
class Pagination:
def __init__(self, page: int = 1, size: int = 20):
self.page = page
self.size = size
self.offset = (page - 1) * size
# 形态四:类实例(__call__ 方法)
class RateLimiter:
def __init__(self, max_calls: int):
self.max_calls = max_calls
async def __call__(self, request: Request):
# 限流逻辑
return True
rate_limiter = RateLimiter(max_calls=100)
# 使用各种形态
@app.get("/example")
async def example(
settings: dict = Depends(get_settings), # 普通函数
pagination: Pagination = Depends(), # 类(省略括号)
limited: bool = Depends(rate_limiter), # 类实例
):
...
💡 使用类作为依赖的快捷语法 :Depends() 不传参数时,FastAPI 会自动使用参数的类型注解作为依赖来源。pagination: Pagination = Depends() 等价于 pagination: Pagination = Depends(Pagination)。
二、分层架构实践
前两篇构建了 shop-api 的项目骨架和路由层,本篇为路由层注入真正的业务逻辑。在动手写代码前,先明确三层架构的职责边界------这是构建可维护系统的基石。
2.1 三层职责边界
Router(路由层)
职责:HTTP 协议处理、参数解析、请求/响应 Schema 转换
类比:Spring MVC 的 @RestController
规则:不包含任何业务逻辑,只做"翻译"
Service(服务层)
职责:业务逻辑编排、事务边界、跨 Repository 协调
类比:Spring 的 @Service
规则:不直接操作 HTTP,不感知请求/响应 Schema
Repository(数据访问层)
职责:数据库 CRUD、查询构建、缓存交互
类比:Spring Data JPA 的 @Repository / JpaRepository
规则:不包含业务逻辑,只做数据映射
2.2 与 Spring 分层的完整对照
| 角色 | Spring Boot | FastAPI shop-api |
|---|---|---|
| 入口控制器 | @RestController ProductController |
APIRouter in routers/products.py |
| 业务服务 | @Service ProductService |
class ProductService in services/product_service.py |
| 数据访问 | @Repository ProductRepository extends JpaRepository |
class ProductRepository in repositories/product_repo.py |
| 数据库会话 | @Transactional(隐式 EntityManager) |
Depends(get_db) → AsyncSession(显式注入) |
| 分页参数 | Pageable pageable(Spring Data 自动绑定) |
CommonParams = Depends()(手动声明) |
| 认证用户 | @AuthenticationPrincipal UserDetails user |
current_user = Depends(get_current_user) |
2.3 目录结构
执行完本篇所有代码后,shop-api 的目录结构如下:
shop-api/
├── app/
│ ├── __init__.py
│ ├── main.py # 第 01 篇:lifespan、CORS、路由注册
│ ├── database.py # 第 04 篇将实现(本篇先占位导入)
│ ├── dependencies/ # 🆕 本篇新增
│ │ ├── __init__.py
│ │ ├── database.py # get_db:数据库 Session 注入
│ │ ├── pagination.py # CommonParams:分页参数
│ │ └── auth.py # get_current_user:认证骨架
│ ├── repositories/ # 🆕 本篇新增骨架
│ │ ├── __init__.py
│ │ └── product_repo.py
│ ├── services/ # 🆕 本篇新增骨架
│ │ ├── __init__.py
│ │ └── product_service.py
│ ├── routers/
│ │ └── products.py # 第 02 篇已有,本篇注入 Service
│ └── schemas/
│ └── product.py # 第 02 篇:Pydantic Schema
└── pyproject.toml
三、数据库 Session 注入
数据库连接管理是后端开发中最容易出错的环节之一。Spring 的 @Transactional 注解把 Session 的开启、提交、回滚全部隐藏在切面(AOP)背后------开发者几乎感知不到。FastAPI 采用相反的哲学:显式优于隐式,通过 yield 依赖让 Session 的生命周期清晰可见。
3.1 get_db 依赖完整实现
首先,在第 04 篇实现数据库引擎之前,app/database.py 需要一个占位文件:
python
# app/database.py(占位,第 04 篇将完整实现)
# 第 04 篇会定义:engine、Base、AsyncSessionLocal
# 这里仅用于让其他模块的 import 不报错
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
# 占位配置,第 04 篇替换为真实数据库 URL
DATABASE_URL = "mysql+aiomysql://user:password@localhost:3306/shop_db"
engine = create_async_engine(
DATABASE_URL,
echo=False,
pool_size=10, # 连接池基础大小(类比 HikariCP minimumIdle);SQLite 不支持连接池,切换 MySQL 后生效
max_overflow=20, # 超出 pool_size 后最多额外创建的连接数(类比 HikariCP maximumPoolSize - minimumIdle);SQLite 不支持连接池,切换 MySQL 后生效
)
AsyncSessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False, # 提交后不自动过期对象,避免 LazyInitializationException 类似问题
autocommit=False,
autoflush=False,
)
⚠️
pool_size和max_overflow说明 :pool_size=10表示连接池维持 10 个常驻连接;当并发超过 10 时,允许再创建最多 20 个临时连接,总上限为 30。超过 30 会等待或报QueuePool limit错误。这与 HikariCP 的minimumIdle+maximumPoolSize语义完全对应。
现在实现核心的 get_db 依赖:
python
# app/dependencies/database.py
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import AsyncSessionLocal # 第 04 篇会定义,这里先占位
async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""
数据库 Session 依赖注入函数
类比 Spring @Transactional 的隐式 Session 管理,但这里是显式的
用 yield 确保 Session 无论如何都会被关闭
"""
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit() # 路由正常完成时提交
except Exception:
await session.rollback() # 发生异常时回滚
raise
3.2 为什么用 yield 而非 return
这是 FastAPI 初学者最常见的疑惑点。yield 将函数变成了一个生成器(Generator),FastAPI 利用这一特性实现了"请求前后"的资源管理:
请求到达
↓
执行 yield 前的代码:创建 AsyncSession
↓
yield session → Session 被注入路由函数
↓
路由函数执行(读写数据库)
↓
路由函数返回(或抛出异常)
↓
执行 yield 后的代码:commit 或 rollback,关闭 Session
↓
响应发送给客户端
与 Spring @Transactional 的对照:
java
// Spring:AOP 自动在方法前后插入事务逻辑(隐式)
@Transactional
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow(...);
// 方法返回后,Spring 自动 commit/rollback
}
python
# FastAPI:yield 依赖显式声明 Session 生命周期
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session # 相当于"进入事务"
await session.commit() # 相当于 commit
except Exception:
await session.rollback() # 相当于 rollback
raise
💡 记忆技巧 :把
yield看成一扇"旋转门"------请求进来时推开一侧(创建 Session),请求出去时推开另一侧(关闭 Session)。无论请求成功还是异常,旋转门都会完整转一圈。
3.3 连接池调优参考
| 参数 | 推荐值 | 说明 |
|---|---|---|
pool_size |
CPU核心数 × 2 | 常驻连接数,对应 HikariCP minimumIdle |
max_overflow |
pool_size × 2 |
突发连接上限,超过后排队等待 |
pool_timeout |
30 | 等待连接超时(秒) |
pool_recycle |
1800 | 连接最长存活时间(秒),防止 MySQL 的 wait_timeout 断开 |
pool_pre_ping |
True |
使用前检测连接是否有效,类比 HikariCP connectionTestQuery |
四、公共参数提取
路由函数中重复出现的参数(如分页、排序、过滤条件)是依赖注入的天然候选。FastAPI 允许将这些参数封装成一个类,通过 Depends() 统一注入------这与 Spring Data 的 Pageable 参数绑定机制高度相似。
4.1 CommonParams 分页依赖
python
# app/dependencies/pagination.py
from dataclasses import dataclass
from typing import Literal
from fastapi import Query
@dataclass
class CommonParams:
"""
通用分页参数依赖类
用法:params: CommonParams = Depends()
类比 Spring 的 @PageableDefault Pageable pageable 参数
"""
page: int = Query(1, ge=1, description="页码,从 1 开始")
size: int = Query(20, ge=1, le=100, description="每页数量,最大 100")
sort_by: str = Query("created_at", description="排序字段")
sort_order: Literal["asc", "desc"] = Query("desc", description="排序方向")
@property
def offset(self) -> int:
return (self.page - 1) * self.size
使用时极为简洁:
python
@router.get("", response_model=list[ProductResponse])
async def list_products(
params: CommonParams = Depends(), # 四个查询参数自动绑定
service: ProductService = Depends(get_product_service),
):
# params.offset 已经帮你算好了分页偏移量
return await service.list_products(offset=params.offset, limit=params.size)
对应的请求 URL:GET /products?page=2&size=10&sort_by=price&sort_order=asc
与 Spring Data 的对照:
java
// Spring Data:框架自动解析 Pageable(固定参数名:page/size/sort)
@GetMapping
public Page<ProductDTO> list(
@PageableDefault(size = 20, sort = "createdAt", direction = DESC) Pageable pageable
) { ... }
python
# FastAPI:显式声明参数,灵活配置默认值和校验规则
@router.get("")
async def list_products(params: CommonParams = Depends()):
# params.offset, params.size, params.sort_by, params.sort_order
...
4.2 商品过滤参数依赖
除分页外,商品列表通常还需要过滤参数。同样用依赖类封装:
python
# app/dependencies/pagination.py(续)
from typing import Optional
@dataclass
class ProductFilter:
"""
商品列表过滤参数
用法:filters: ProductFilter = Depends()
"""
category_id: Optional[int] = Query(None, description="分类 ID 过滤")
keyword: Optional[str] = Query(None, min_length=1, max_length=50, description="搜索关键词")
min_price: Optional[float] = Query(None, ge=0, description="最低价格")
max_price: Optional[float] = Query(None, ge=0, description="最高价格")
is_on_sale: Optional[bool] = Query(None, description="是否在售")
def to_dict(self) -> dict:
"""转为字典,过滤掉 None 值,方便传给 Repository"""
return {k: v for k, v in vars(self).items() if v is not None}
在路由中组合使用分页和过滤:
python
@router.get("", response_model=list[ProductResponse])
async def list_products(
params: CommonParams = Depends(),
filters: ProductFilter = Depends(),
service: ProductService = Depends(get_product_service),
):
return await service.list_products(
offset=params.offset,
limit=params.size,
sort_by=params.sort_by,
sort_order=params.sort_order,
filters=filters.to_dict(),
)
4.3 依赖作为类(__call__)vs 依赖作为函数
当依赖需要在"初始化阶段"接收配置、在"调用阶段"处理请求时,__call__ 模式非常有用:
python
# 场景:需要配置最大请求频率的限流器
class RateLimiter:
"""
可配置的限流依赖类
使用 __call__ 模式:初始化时传入配置,每次请求时执行限流检查
类比 Spring AOP 切面的 @Around,在请求前后执行逻辑
"""
def __init__(self, calls_per_minute: int = 60):
self.calls_per_minute = calls_per_minute
self._call_records: dict = {}
async def __call__(self, request: Request) -> bool:
client_ip = request.client.host
now = time.time()
# 简化版限流逻辑(生产中用 Redis 实现)
window_start = now - 60
calls = [t for t in self._call_records.get(client_ip, []) if t > window_start]
if len(calls) >= self.calls_per_minute:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="请求过于频繁,请稍后再试"
)
calls.append(now)
self._call_records[client_ip] = calls
return True
# 在应用级别创建实例(配置注入),在请求级别调用(请求注入)
standard_limiter = RateLimiter(calls_per_minute=60)
strict_limiter = RateLimiter(calls_per_minute=10)
# 路由级别使用不同的限流策略
@router.post("", dependencies=[Depends(standard_limiter)])
async def create_product(...): ...
@router.delete("/{product_id}", dependencies=[Depends(strict_limiter)])
async def delete_product(...): ...
📝
dependencies参数 :路由装饰器的dependencies参数接受一个依赖列表,这些依赖会被执行,但其返回值不会注入到路由函数参数中。适合只需要"执行副作用"(如限流、审计日志)的场景。
五、当前用户注入
认证是几乎所有 API 服务的必备功能。本篇先搭建认证依赖的骨架------完整的 JWT 解析将在第 05 篇实现。
5.1 User Schema 占位定义
在实现认证依赖前,先定义一个占位的 User Schema:
python
# app/schemas/user.py(占位,第 05 篇完善)
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
"""
当前登录用户信息
第 05 篇会补充 JWT 解析逻辑和完整字段
"""
id: int
username: str
email: str
is_active: bool = True
is_admin: bool = False
5.2 get_current_user 骨架
python
# app/dependencies/auth.py
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
# OAuth2PasswordBearer 会在 OpenAPI 文档中生成"Authorize"按钮
# tokenUrl 指向获取 Token 的端点(第 05 篇实现)
# auto_error=False 表示 Token 不存在时不自动报 401,而是返回 None
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token", auto_error=False)
async def get_current_user(token: Optional[str] = Depends(oauth2_scheme)):
"""
获取当前登录用户
第 05 篇会完善此函数,接入 JWT 校验
当前版本仅作占位,返回 None
"""
if token is None:
return None
# TODO: 第 05 篇实现 JWT 解析
# payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# user_id = payload.get("sub")
# return await user_service.get_user(user_id)
return None
async def require_auth(current_user = Depends(get_current_user)):
"""
要求登录的依赖,未登录时返回 401
类比 Spring Security @PreAuthorize("isAuthenticated()")
"""
if current_user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="需要登录",
headers={"WWW-Authenticate": "Bearer"},
)
return current_user
async def require_admin(current_user = Depends(require_auth)):
"""
要求管理员权限
类比 Spring Security @PreAuthorize("hasRole('ADMIN')")
依赖嵌套:require_admin 依赖 require_auth,require_auth 依赖 get_current_user
"""
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限",
)
return current_user
5.3 在路由中使用认证依赖
python
# routers/products.py(认证集成示例)
from app.dependencies.auth import get_current_user, require_auth, require_admin
from app.schemas.user import User
# 公开接口:不需要登录
@router.get("", response_model=list[ProductResponse])
async def list_products(
params: CommonParams = Depends(),
service: ProductService = Depends(get_product_service),
):
return await service.list_products(offset=params.offset, limit=params.size)
# 需要登录:获取当前用户购物车等个性化数据时使用
@router.get("/{product_id}", response_model=ProductResponse)
async def get_product(
product_id: int,
service: ProductService = Depends(get_product_service),
current_user: Optional[User] = Depends(get_current_user), # 可选登录
):
product = await service.get_product(product_id)
# 未来可根据 current_user 决定是否展示会员价等信息
return product
# 需要管理员权限:创建商品
@router.post("", response_model=ProductResponse, status_code=status.HTTP_201_CREATED)
async def create_product(
product: ProductCreate,
service: ProductService = Depends(get_product_service),
_: User = Depends(require_admin), # 用 _ 表示只需要鉴权,不使用返回值
):
return await service.create_product(product)
💡
_命名惯例 :当依赖只用于"执行副作用"(如鉴权),不需要使用其返回值时,将参数命名为_是 Python 社区的通用惯例,明确表示"我知道有返回值,但我不需要它"。
六、完整路由层实现
现在将所有依赖整合,实现完整的 products.py 路由文件:
6.1 Repository 骨架
python
# app/repositories/product_repo.py(骨架,第 04 篇完善)
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.product import ProductCreate, ProductUpdate
class ProductRepository:
"""
商品数据访问层
类比 Spring Data JPA 的 JpaRepository<Product, Long>
第 04 篇会实现真正的 SQLAlchemy ORM 操作
"""
def __init__(self, db: AsyncSession):
self.db = db
async def find_all(self, offset: int = 0, limit: int = 20) -> list:
# 第 04 篇实现:return await db.execute(select(Product).offset(offset).limit(limit))
return []
async def find_by_id(self, product_id: int) -> Optional[dict]:
# 第 04 篇实现:return await db.get(Product, product_id)
return None
async def create(self, data: ProductCreate) -> dict:
# 第 04 篇实现:ORM 对象创建 + flush + refresh
return {"id": 1, **data.model_dump()}
async def update(self, product_id: int, data: ProductUpdate) -> Optional[dict]:
# 第 04 篇实现:partial update with model_dump(exclude_unset=True)
return None
async def delete(self, product_id: int) -> None:
# 第 04 篇实现:软删除或硬删除
pass
6.2 Service 骨架
python
# app/services/product_service.py(骨架,第 04 篇完善)
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.product_repo import ProductRepository
from app.schemas.product import ProductCreate, ProductUpdate
class ProductService:
def __init__(self, db: AsyncSession):
self.repo = ProductRepository(db)
async def list_products(
self,
offset: int,
limit: int,
sort_by: str = "created_at",
sort_order: str = "desc",
filters: Optional[dict] = None,
):
# 业务逻辑层:校验参数、权限检查、数据转换
return await self.repo.find_all(offset=offset, limit=limit)
async def get_product(self, product_id: int):
product = await self.repo.find_by_id(product_id)
if not product:
from fastapi import HTTPException, status
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"商品 {product_id} 不存在"
)
return product
async def create_product(self, data: ProductCreate):
# 未来可在这里加:检查商品名称唯一性、校验分类是否存在等业务规则
return await self.repo.create(data)
async def update_product(self, product_id: int, data: ProductUpdate):
# 先检查商品是否存在(复用 get_product 的 404 逻辑)
await self.get_product(product_id)
return await self.repo.update(product_id, data)
async def delete_product(self, product_id: int):
await self.get_product(product_id) # 检查存在性
await self.repo.delete(product_id)
6.3 完整 Router
python
# app/routers/products.py(完整版,集成 Service + 分页 + 认证)
from typing import Optional
from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies.database import get_db
from app.dependencies.pagination import CommonParams, ProductFilter
from app.dependencies.auth import get_current_user, require_admin
from app.services.product_service import ProductService
from app.schemas.product import ProductCreate, ProductResponse, ProductUpdate
from app.schemas.user import User
router = APIRouter(prefix="/products", tags=["商品"])
def get_product_service(db: AsyncSession = Depends(get_db)) -> ProductService:
"""
依赖工厂函数:创建 ProductService 实例,注入 DB Session
类比 Spring 的构造器注入:new ProductService(db)
每次请求创建新的 Service 实例(无状态,线程安全)
"""
return ProductService(db)
# ─── 商品列表 ────────────────────────────────────────────────────────────────
@router.get(
"",
response_model=list[ProductResponse],
summary="商品列表",
description="支持分页、排序和多条件过滤",
)
async def list_products(
params: CommonParams = Depends(),
filters: ProductFilter = Depends(),
service: ProductService = Depends(get_product_service),
):
return await service.list_products(
offset=params.offset,
limit=params.size,
sort_by=params.sort_by,
sort_order=params.sort_order,
filters=filters.to_dict(),
)
# ─── 商品详情 ────────────────────────────────────────────────────────────────
@router.get(
"/{product_id}",
response_model=ProductResponse,
summary="商品详情",
)
async def get_product(
product_id: int,
service: ProductService = Depends(get_product_service),
):
return await service.get_product(product_id)
# ─── 创建商品(需要管理员权限)───────────────────────────────────────────────
@router.post(
"",
response_model=ProductResponse,
status_code=status.HTTP_201_CREATED,
summary="创建商品",
)
async def create_product(
product: ProductCreate,
service: ProductService = Depends(get_product_service),
_: User = Depends(require_admin),
):
return await service.create_product(product)
# ─── 更新商品(需要管理员权限)───────────────────────────────────────────────
@router.patch(
"/{product_id}",
response_model=ProductResponse,
summary="更新商品",
)
async def update_product(
product_id: int,
product: ProductUpdate,
service: ProductService = Depends(get_product_service),
_: User = Depends(require_admin),
):
return await service.update_product(product_id, product)
# ─── 删除商品(需要管理员权限)───────────────────────────────────────────────
@router.delete(
"/{product_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="删除商品",
)
async def delete_product(
product_id: int,
service: ProductService = Depends(get_product_service),
_: User = Depends(require_admin),
):
await service.delete_product(product_id)
七、依赖的生命周期
理解依赖的生命周期,是排查资源泄漏和性能问题的关键。FastAPI 中有三种级别的资源生命周期。
7.1 请求级:yield 依赖
最常见的模式,每次 HTTP 请求创建一次资源,请求结束时释放:
python
# 典型场景:数据库 Session、外部 HTTP Client
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session # 请求期间可用
await session.commit()
except Exception:
await session.rollback()
raise
# Session 在 async with 块结束时自动关闭
生命周期可视化:
请求1: [创建 Session] ──────────── [commit] [关闭 Session]
请求2: [创建 Session] ──────── [rollback] [关闭 Session]
请求3: [创建 Session] ──── [commit] [关闭 Session]
↑ ↑
请求到达 响应发送
7.2 应用级:lifespan 资源
需要在整个应用生命周期内共享的资源(如数据库引擎、Redis 连接池),应在 lifespan 中管理。回顾并完善第 01 篇的 main.py:
python
# app/main.py(完善版,引用第 01 篇骨架)
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
# 应用级资源(整个应用生命周期内共享)
app_state = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
应用级资源管理
类比 Spring Boot 的 ApplicationRunner + @PreDestroy
yield 前:应用启动时执行(等价于 @PostConstruct)
yield 后:应用关闭时执行(等价于 @PreDestroy)
"""
# ── 启动阶段 ──────────────────────────────────────────
print("🚀 shop-api 启动中...")
# 数据库引擎(第 04 篇中定义,这里引用)
# from app.database import engine
# await engine.connect() # 预热连接池
# app_state["db_engine"] = engine
# Redis 连接池(第 06 篇)
# import aioredis
# redis = await aioredis.create_redis_pool("redis://localhost")
# app_state["redis"] = redis
print("✅ 所有资源初始化完成")
yield # 应用正常运行期间
# ── 关闭阶段 ──────────────────────────────────────────
print("🛑 shop-api 关闭中,释放资源...")
# if "db_engine" in app_state:
# await app_state["db_engine"].dispose()
# if "redis" in app_state:
# app_state["redis"].close()
# await app_state["redis"].wait_closed()
print("✅ 资源释放完成")
app = FastAPI(
title="shop-api",
version="0.1.0",
lifespan=lifespan,
)
# CORS 配置(第 01 篇已实现,此处保留)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 路由注册
from app.routers import products
app.include_router(products.router, prefix="/api/v1")
7.3 三种生命周期对比
函数级 return 依赖
请求到达
执行依赖函数
返回值注入路由
路由函数执行
请求级 yield 依赖
请求到达
创建 AsyncSession
路由函数执行
commit/rollback
关闭 Session
应用级 lifespan
应用启动
数据库引擎
Redis 连接池
应用运行中...
应用关闭
7.4 dependency_overrides:测试时替换依赖
FastAPI 提供 app.dependency_overrides 字典,允许在测试中替换任意依赖------无需修改业务代码,只需在测试 fixture 中覆盖即可。这将在第 10 篇(测试专题)中详细讲解,这里先看用法预告:
python
# tests/conftest.py(预告,第 10 篇详细实现)
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.dependencies.database import get_db
@pytest.fixture
async def test_client():
"""
测试客户端 fixture
使用内存 SQLite 替换真实数据库依赖
类比 Spring Boot 的 @SpringBootTest + @AutoConfigureTestDatabase
"""
# 创建测试专用的内存数据库 Session
test_db = create_test_db_session()
# 覆盖真实的 get_db 依赖
app.dependency_overrides[get_db] = lambda: test_db
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
yield client
# 测试结束后恢复原始依赖
app.dependency_overrides.clear()
🎯 设计亮点 :
dependency_overrides不需要任何 Mock 框架,只需一个字典操作即可完成依赖替换。这比 Spring Boot 中配置@MockBean更简单直接。
八、常见坑与最佳实践
经过大量生产实践,以下是使用 FastAPI 依赖注入时最常踩的坑。
8.1 Session 过早关闭
❌ 错误写法:在 Service 层内部创建 Session
python
# ❌ 糟糕:Session 在函数内部创建,无法被测试替换,也无法跨 Service 共享事务
class ProductService:
async def get_product(self, product_id: int):
async with AsyncSessionLocal() as session: # 每次调用创建新 Session
product = await session.get(Product, product_id)
return product
✅ 正确写法:Session 通过构造器注入
python
# ✅ 正确:Session 从外部注入,生命周期由依赖系统管理
class ProductService:
def __init__(self, db: AsyncSession):
self.db = db # Session 由调用方(Depends)传入
async def get_product(self, product_id: int):
product = await self.db.get(Product, product_id)
return product
8.2 在依赖中抛出非 HTTPException
❌ 错误写法:在依赖中抛出普通异常
python
# ❌ 糟糕:普通 ValueError 会导致 500 Internal Server Error,而非有意义的 4xx 响应
async def require_auth(token: str = Depends(oauth2_scheme)):
if token is None:
raise ValueError("Token is required") # 客户端收到 500,体验极差
return token
✅ 正确写法:在依赖中抛出 HTTPException
python
# ✅ 正确:HTTPException 会被 FastAPI 转换为对应的 HTTP 状态码响应
async def require_auth(token: Optional[str] = Depends(oauth2_scheme)):
if token is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="需要提供 Bearer Token",
headers={"WWW-Authenticate": "Bearer"},
)
return token
8.3 误用 Depends() 的缓存行为
❌ 错误认知:以为每次使用都会重新执行依赖函数
python
# ❌ 误解:以下代码中,get_db 只会被调用一次,两个 service 共享同一个 Session
@router.get("/complex")
async def complex_operation(
product_service: ProductService = Depends(get_product_service),
# get_product_service 内部依赖 get_db
# 如果 order_service 也依赖 get_db,默认情况下它们共享同一个 Session!
order_service: OrderService = Depends(get_order_service),
):
...
✅ 正确理解:同一请求中,相同依赖默认只执行一次(缓存)
python
# ✅ 正确:同一请求中共享 Session 是期望行为(保证事务一致性)
# 如果确实需要独立 Session,使用 use_cache=False
@router.get("/independent-sessions")
async def with_independent_sessions(
db1: AsyncSession = Depends(get_db), # Session A
db2: AsyncSession = Depends(get_db, use_cache=False), # Session B(强制重新执行)
):
# db1 和 db2 是两个独立的 Session
...
8.4 全局路由依赖 vs 单个路由依赖
❌ 错误做法:为每个路由重复声明相同的依赖
python
# ❌ 糟糕:每个路由都声明认证依赖,重复且容易漏写
@router.get("", dependencies=[Depends(require_auth)])
async def list_products(...): ...
@router.post("", dependencies=[Depends(require_auth)])
async def create_product(...): ...
@router.delete("/{id}", dependencies=[Depends(require_auth)])
async def delete_product(...): ...
✅ 正确做法:在 Router 级别或 include_router 时声明全局依赖
python
# ✅ 正确:Router 级别统一声明,所有路由自动继承
# 方式一:在 APIRouter 创建时声明(该 Router 下所有路由生效)
admin_router = APIRouter(
prefix="/admin/products",
tags=["管理员-商品"],
dependencies=[Depends(require_admin)], # 整个 Router 要求管理员权限
)
# 方式二:在 include_router 时声明(更灵活)
app.include_router(
products.router,
prefix="/api/v1",
dependencies=[Depends(require_auth)], # 该 Router 下所有路由要求登录
)
8.5 依赖函数忘记 async
❌ 错误写法:异步操作放在同步依赖函数中
python
# ❌ 糟糕:同步函数中执行异步 I/O,会阻塞事件循环
def get_user_sync(token: str = Depends(oauth2_scheme)):
user = asyncio.run(fetch_user_from_db(token)) # 🚫 严禁在异步上下文中用 asyncio.run
return user
✅ 正确写法:异步 I/O 操作使用 async 依赖函数
python
# ✅ 正确:I/O 操作必须用 async def
async def get_current_user(token: Optional[str] = Depends(oauth2_scheme)):
if token is None:
return None
user = await fetch_user_from_db(token) # 非阻塞异步调用
return user
九、总结
9.1 核心概念速查表
| 概念 | FastAPI | Spring Boot 对标 | 关键特点 |
|---|---|---|---|
| 依赖声明 | Depends(func) |
@Autowired |
请求级解析,非容器持有 |
| 数据库会话 | yield 依赖 + AsyncSession |
@Transactional + EntityManager |
显式生命周期,显式提交/回滚 |
| 分页参数 | CommonParams = Depends() |
Pageable pageable |
自动绑定查询参数到 dataclass |
| 认证注入 | Depends(get_current_user) |
@AuthenticationPrincipal |
可选/必选两种模式 |
| 权限控制 | Depends(require_admin) |
@PreAuthorize |
依赖嵌套实现权限链 |
| 应用级资源 | lifespan + yield |
@PostConstruct + @PreDestroy |
引擎/连接池等长生命周期资源 |
| 测试替换 | dependency_overrides |
@MockBean |
无需 Mock 框架,字典操作替换 |
| 全局依赖 | APIRouter(dependencies=[...]) |
@PreAuthorize on class |
Router 级统一声明 |
9.2 依赖嵌套关系图
HTTP 请求
Router 路由函数
get_product_service
CommonParams
require_admin
get_db
AsyncSessionLocal
require_auth
get_current_user
oauth2_scheme
🎯 本篇金句 :依赖注入的本质不是"框架帮你创建对象",而是"代码声明我需要什么,框架保证你能拿到"------FastAPI 的
Depends()将这种契约精神贯彻到了每一次 HTTP 请求。
参考资料
- FastAPI 官方文档 - Dependencies
- FastAPI 官方文档 - Dependencies with yield
- FastAPI 官方文档 - Global Dependencies
- SQLAlchemy 2.0 异步文档
- Python dataclasses 官方文档
- OAuth2 with Password (and hashing), Bearer with JWT tokens
下期预告
第 04 篇:SQLAlchemy 2.0 异步 ORM------用 Model 取代骨架
本篇中所有 Repository 方法都是空壳,第 04 篇将彻底填满它们:
- SQLAlchemy 2.0 的
DeclarativeBase定义 Product Model - 异步 CRUD:
session.execute(select(...))vs Spring Data JPA 的findAll() - 关系映射:一对多(Product → Image)、多对多(Product ↔ Tag)
- 迁移工具:Alembic 与 Flyway 的对比
- 查询构建器:
select().where().order_by().offset().limit()完整示例 - N+1 问题:
selectinloadvsjoinedload的选择
📝 预计难度:★★★★☆------SQLAlchemy 2.0 的异步 API 与 1.x 差异较大,需要重新建立心智模型。