07. FastAPI——依赖注入(Depends)
一、什么是依赖注入?
依赖(Dependency) 是指一些可以被多个接口重复使用的功能模块/通用逻辑。
例如:登录校验、数据库连接、分页参数、权限判断等。
这些功能可以写成函数或类,供多个接口共同使用。
依赖注入(Dependency Injection) 是指 FastAPI 在执行路径操作函数(接口函数)之前,会先自动执行依赖函数,然后把依赖函数的返回结果作为参数传递给路径操作函数使用。
所以,在开发过程中使用依赖注入,可以把通用逻辑单独拆分出来,实现通用逻辑的复用和共享,减少代码重复。
二、如果没有依赖注入
实际场景:在访问某些接口时,必须验证登录信息。比如:查看个人信息、查看订单信息和修改密码等。最原始、最直接的方式就是在这些需要验证登录信息的接口里都写一遍登录校验的逻辑,但这会让整个项目出现许多重复代码:
# 接口1:返回用户信息
@app.get("/user/info")
async def user_info(token: str = Query(...)):
if token != "123":
raise HTTPException(status_code=401, detail="未登录")
return {"name": "alex"}
# 接口2:返回订单列表
@app.get("/order/list")
async def order_list(token: str = Query(...)):
if token != "123": # 逻辑重复
raise HTTPException(status_code=401, detail="未登录")
return {"order": "订单列表"}
可以发现上面的每个接口都在重复写登录校验逻辑:
if token != "123":
raise HTTPException(status_code=401, detail="未登录")
三、依赖注入的代码示例
那么,如何把重复的登录校验逻辑封装成依赖项呢?使用 fastapi 提供的 Depends 即可:
# 1.首先引入 Depends
from fastapi import FastAPI, Depends, HTTPException, Query
app = FastAPI()
# 2.定义登录校验依赖项。其实就是把登录校验逻辑写入一个异步函数中
# 注:在实际开发中一般token都会放在HTTP请求头中,而非查询参数中,这里只是为了举依赖注入的例子
async def check_login(token: str = Query(...)):
if token != "123":
raise HTTPException(status_code=401, detail="未登录")
return {"user": "alex"}
# 3.注入依赖
@app.get("/user/info")
async def user_info(user = Depends(check_login)): # 注入。注意注入时check_login没有小括号
return {"msg": "这是用户信息", "user": user}
@app.get("/order/list")
async def order_list(user = Depends(check_login)): # 注入
return {"msg": "这是订单列表", "user": user}
可以看到上述代码使用三个步骤完成了依赖注入:
1. 引入Depends → 2. 定义依赖项 → 3. 依赖注入
其实依赖注入就是依靠Depends去调用依赖,在执行路由函数之前 Depends 先帮你执行一个函数,然后把执行的结果给你的接口函数用。
但是,Depends 能调用的依赖,手动也可实现,为啥非要用 Depends 不可呢?以上述代码的 /user/info 接口为例,手动调用 check_login 函数时的代码:
@app.get("/user/info")
async def user_info(token: str): # 这里需要手动传递token这个参数
user = await check_login(token) # 这里需要手动await依赖项
return {"msg": "这是用户信息", "user": user}
可以直观地看到,没有 Depends 时,就只能手动传递并注解参数,再手动 await 调用依赖项。所以 Depends 不只是“函数调用简写”,而是 FastAPI 的“依赖解析系统”,负责参数解析、依赖执行、依赖嵌套、生命周期管理等(下面的表格涉及的功能需要在实际开发中慢慢积累经验):
| 功能 | 手动调用 | Depends |
|---|---|---|
| 调用函数 | ✔ | ✔ |
| await | ✔ | ✔ |
| 从 Query / Header / Cookie 取参数 | ✔(手动写) | ✔(自动) |
| 多个依赖嵌套 | ✖ 很麻烦 | ✔ |
| 依赖缓存(同一请求只执行一次) | ✖ | ✔ |
| 数据库 session 生命周期 | ✖ | ✔ |
| 可用于 WebSocket | ✖ | ✔ |
| 可用于全局依赖 | ✖ | ✔ |
小细节
在执行依赖函数 async def check_login(token: str = Query(...)) 时,由于 token 被 Query(...) 声明为必填参数,如果请求中没有携带 token,FastAPI 会在参数校验阶段直接返回 422 错误(参数缺失),此时依赖函数和路径操作函数都不会执行。
但在实际项目中,前端通常会根据接口返回的状态码进行处理:如果返回 401(未登录),前端会跳转到登录页面;如果返回 422(参数错误),前端通常会提示用户请求参数有误。
所以,实际开发中更常见的做法是:即使没有 token,后端逻辑也应写为主动返回 401 未登录,而不是使用 Query(...) 强制校验为 422。尽量把系统错误转换为可读的业务错误,并通过 HTTP 状态码和错误信息返回给客户端,这样可以让前端和用户更容易理解错误原因。
四、补充
1、依赖注入的常用场景
| 场景 | 用 Depends |
|---|---|
| 登录校验 | Depends(check_login) |
| 获取当前用户 | Depends(get_current_user) |
| 数据库连接 | Depends(get_db) |
| 分页参数 | Depends(get_page) |
| 权限校验 | Depends(check_admin) |
| 日志记录 | Depends(log_request) |
2、中间件 vs 依赖注入
中间件也可以存放通用逻辑,那为什么还需要依赖注入呢?
因为在每次 请求处理前 和 响应返回前 都要经过中间件,所以不论访问哪个接口,都要经过中间件的逻辑。但是,在实际开发中,所有接口的业务逻辑肯定是有所区别的,比如:如果我在中间件里面放入了登陆信息校验逻辑,那么我访问所有接口是都必须是登录状态下访问,但是实际的网站中有些页面/接口不需要用户登录也能访问,这时候就需要把登录校验逻辑封装成依赖,再根据每个接口负责的功能和逻辑适当进行注入。
所以,中间件适合做全局逻辑(日志、跨域、统一异常处理、请求时间统计等),Depends 适合做接口级别的逻辑(登录校验、权限校验、数据库会话、分页参数等)。
3、Depends 的依赖缓存机制
FastAPI 的 Depends 具有依赖缓存机制,在同一次请求中,同一个依赖函数只会执行一次,可以避免重复执行数据库查询等操作。
代码示例:
async def get_user():
print("执行了")
return {"name": "alex"}
@app.get("/test")
# 参数默认值获取时重复依赖注入Depends(get_user)
async def test(user1 = Depends(get_user), user2 = Depends(get_user)):
return {"u1": user1, "u2": user2}
当 /test 接口接到请求时,test 函数的两个参数 user1 和 user2 都是需要异步执行 get_user 获得默认值的,但是通过Depends 去调用 get_user 时,会先查找请求作用域(request scope)内是否有 get_user 的执行结果。如果有,则直接拿出来给 user1 和 user2,如果请求作用域内没有 get_user 的执行结果,则执行 get_user,并将结果赋给 user1 并存入请求作用域,此时请求作用域中有了 get_user 的执行结果,则下一个参数 user2 直接用缓存数据即可。
通过上述例子可以看出 Depends 具有“请求级缓存”,同一个依赖函数在一次请求中只会执行一次,多个 Depends 会共享同一个结果。
更多推荐
所有评论(0)