封面《彼女は高天に祈らない -quantum girlfriend-》
前言
因为在写 nonebot 插件的时候觉得 nonebot 的依赖注入非常的神奇,就读了一下源码,发现其实原理很简单,但是与 Java 的 Spring 框架相比还是不太一样,因此就写一下笔记并自己实现一下核心代码。
nonebot 中的依赖注入
在 nonebot2 中我们对于一个插件部分的响应函数一般这样写
1 |
|
在运行过程中,nonebot2 框架在收到消息时会根据事件参数生成 Event,收到消息的机器人 id 生成 Bot 参数,然后按照优先级遍历 matcher 执行函数。此时会将 Bot、Event 等参数注入到我们自己定义的函数中,除了框架自己定义的依赖以外还有 Depends 函数来包装用户自己定义的依赖。
今天主要讲的是 nonebot2 是怎么实现这个依赖注入了
nonebot 流程
nonebot2 框架流程如下所示

下面以 fastapi 作为驱动器和 onebotV11 作为适配器来具体讲一下具体的依赖注入流程,此处只考虑依赖注入相关内容,不探讨参数校 = 校验、Rule、Permission 等
初始化的模板首先是执行 nonebot.init(),这里初始化 driver,默认的 driver 是 fastapi,然后给 driver 注册 adapter。adapter 中通过_setup(self) 方法来注册 http、websocket 的响应函数。
事件的响应函数流程主要如下,此处省去了一些参数检查、token 检查、Bot 和 Event 参数生成、Rule 检查、权限检查还有一些前处理后处理的 hook 函数
adapter._handle_ws(self, websocket: WebSocket)->bot.handle_event(event)->message.handle_event(bot,event)->message._check_matcher(priority, matcher, bot, event, stack, dependency_cache)->message._run_matcher(Matcher, bot, event, state, stack, dependency_cache)->matcher.run(bot, event, state, stack, dependency_cache)->matcher.simple_run(bot, event, state, stack, dependency_cache)->Dependent.__call__(matcher,bot,event,state,stack,dependency_cache)
因此可以看到抛开许多参数的预处理,nonebot 解决依赖注入的核心代码在 Dependent 的__call__函数中
需要注意的是在
message._check_matcher(priority, matcher, bot, event, stack, dependency_cache)这一步中已经将全部预先定义好的参数传入函数,这里不包含用户定义的 Depends,用户定义的 Depends 要到 Dependent.solve 时解决
nonebot 依赖注入核心
依赖注入容器
nonebot2 的依赖注入容器是 Dependent 类,先来看其中的核心函数
1 | class Dependent: |
在运行之前,先对要注入的函数执行 parse 函数,首先通过反射来获取函数的签名和参数。然后生成一个 Dependent 容器。遍历参数,该函数的所有参数包装成 Param 的子类,这里的 Param 是我们依赖注入的最小单元会在后面讲。并且加到 Dependent 容器的 params 里面。
函数参数中带的 * 会使得 * 后面的参数只能通过指定参数名的形式传
再看 solve 函数,定义了一个字典,key 是参数名,value 是参数值。该函数通过遍历自己的 params 参数,通过 param 的_solve 函数来提取对应参数名的参数值。然后将提取到的参数值填充到字典中,这样虽然传进来了许多参数,但是实际执行的注入的参数是函数所拥有的部分。
依赖注入核心
上面我们说了 nonebot2 中依赖注入的核心是 Param 类
1 | class Param(abc.ABC, FieldInfo): |
Param 是一个抽象类,为了方便解释我们放两个具体的实现类,我们主要关注两个函数,_check_param 和_solve,_check_param 函数将符合 Param 的参数包装成 Param 返回否则返回 None,比如 BotParam 只处理 Bot 类,DependParam 只处理 DependInner,同时_check_param 不仅可以通过 annotation 注入也可以通过参数名注入。_solve 函数则是从 **kwargs 中获取对应的值,对于实现已经预定好的 bot 参数因为参数名和参数值都是确定的所以很简单,而 DependParam 因为是用户自己定义的类型包装且需要处理嵌套的子 Dependent,相对处理起来麻烦。
处理流程图
干说有点枯燥,还是来一张流程图吧

实现依赖注入
提取 nonebot2 中依赖注入的核心代码,去除了一些参数检查、matcher 选择和 hook,只保留了参数注入部分。
完整代码在 github
exception.py
主要为解析过程中会遇到的异常
1 | class TypeMisMatch(Exception): |
model.py
主要为预先定义好的一定会出现的参数类型
1 |
|
utils.py
主要为一些工具,检查 override,获取函数参数,检查是否子类等
1 | import inspect |
params.py
主要为依赖注入组件的定义、容器的定义以及对用户自定义类型的包装
1 | from pydantic.fields import FieldInfo, ModelField, Required, Undefined |
测试结果
demo 如下,省去了 nonebot 中通过装饰器注册容器,和选择 handler 运行
1 | from typing import Dict |
运行结果如下

后记
简单实现一下 nonebot2 的依赖注入,其实可以发现里面的逻辑非常简单,可以简化为通过反射获取函数的参数信息,然后将初始化一个字典 values,key 是参数名,value 是参数值,然后将外部的全部参数按照参数名或者参数类型放入字典中,最后通过 **kwargs 的形式执行被注入函数。
参考
Bare asterisk in function arguments?
What is the purpose of a bare asterisk in function arguments?