封面《彼女は高天に祈らない -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?