FastAPI 系列·(三):依赖注入——用 Depends 构建分层架构

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}

这里有几个关键点值得注意:

  1. Depends(get_query_token) 传入的是函数本身(不带括号),FastAPI 负责在正确的时机调用它
  2. 依赖函数的参数同样遵循 FastAPI 的参数解析规则------token: str 会被识别为查询参数
  3. 路由函数只声明"我需要什么",不关心"如何获取"------这正是依赖倒置原则(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_sizemax_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 请求。


参考资料


下期预告

第 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 问题:selectinload vs joinedload 的选择

📝 预计难度:★★★★☆------SQLAlchemy 2.0 的异步 API 与 1.x 差异较大,需要重新建立心智模型。

相关推荐
marsh02063 小时前
56 openclaw与Serverless:无服务器架构下的应用实践
云原生·架构·serverless
SmartBrain3 小时前
AI全栈开发(SDD):慢病管理系统工程级设计
java·大数据·开发语言·人工智能·架构·aigc
zandy10114 小时前
2026 BI平台与数据中台融合架构实践:从数据烟囱到统一智能数据层
大数据·架构·spark
rising start4 小时前
Web认证机制演进
架构·jwt·session
Donk_674 小时前
ELK+Redis架构搭建
redis·elk·架构
龙佚5 小时前
RTC语音质量优化实战:搭建完整语音系统
算法·架构
泥秋哥5 小时前
微前端-Module Federation运行时工具
前端·架构
国科安芯6 小时前
ASM232S抗辐照RS-232收发器的技术架构与空间环境适应性研究
单片机·嵌入式硬件·安全·架构·安全性测试
心中有国也有家6 小时前
PaddlePaddle 适配 NPU 的技术全解析——从算子接入到端到端性能优化
人工智能·分布式·算法·性能优化·架构·paddlepaddle