# 中间件(Middleware)

有时候, 依赖注入在一些奇怪的地方作用的不是太好(例如 Ponyorm 就是用类似 with orm.session 建立连接的), 我们需要在事务运行前就执行代码, 并且我们还需要在事务运行完毕后做一些清理工作, 这时候, 我们就可以用到 中间件(Middleware) 特性轻松的解决这个问题.

# 简单的介绍

在 python 中, 有一个称为 with 的关键字, 即 上下文管理器(Context Manager), 可以先执行任意对象内的 __enter__ 方法, 在执行完代码块后又去执行 __exit__ 方法, 可以说是很方便了, 较之 try-finally 语法要好用的多.

with 也有它的异步版本 async with, 执行的是异步方法 __aenter____aexit__, 与同步方法大同小异.

中间件便是围绕着这个语言特性设计的, 我们同时支持这两种操作方式, 不需要对其进行二次包装.

TIP

如果你用过类似 with open(...) as file 这样的, 你可能会感到疑惑, 因为中间件特性不能使用 as 关键字捕获 __enter__ 的返回值, 所以如果你有这样的需求, 建议还是在业务代码里包含.

# 使用

我们需要在 receiver 里定义参数 middlewares, 传入一系列带有方法 __enter__(或 __aenter__) 和 __exit__(或 __aexit__) 的对象:

@app.receiver(..., middlewares=[
    # 传入一些带有方法 `__enter__`(或 `__aenter__`) 和 `__exit__`(或 `__aexit__`) 的对象
])
async def event_handler(...):
1
2
3
4

我们建议你使用 Python 标准库 contextlib 内提供的一些装饰器函数, 帮助你使用中间件特性.
我们可以使用 contextlib.contextmanager 装饰一个同步函数, 使其可以直接使用:

from contextlib import contextmanager

@contextmanager
def middleware1():
    # 在事务代码执行前会执行的部分
    yield # 后面可以跟任何值, 本来是拿来给 `with as` 用的, 现在用作用不了, 所以传什么值都无所谓
    # 在事务代码执行后会执行的部分
1
2
3
4
5
6
7

当然, contextlib.contextmanager 也有它的异步版本, 即 contextlib.asynccontextmanager, 用于包装异步函数为上下文管理器:

from contextlib import asynccontextmanager

@asynccontextmanager
async def middleware2():
    # 在事务代码执行前会执行的部分
    yield
    # 在事务代码执行后会执行的部分
1
2
3
4
5
6
7

无论是同步版本的 middleware1 还是异步版本的 middleware2, 中间件特性皆支持, 不需要担心兼容性.
无论 contextmanager 还是 asynccontextmanager, 他们都返回了一个可以为 with 使用的对象, 理论上, 一个对象只需要可以为 with 使用, 就可以用来当中间件使用.

# 设置全局中间件

Mirai 对象实例化时, 我们可以通过传入 global_middlewares 使所有的事件监听处理 (包括 exception handler, 但不包括 subroutine) 都会调用该列表内的上下文管理器:

app = Mirai(..., global_middlewares=[
    middleware1,
    middleware2,
    ...
])
1
2
3
4
5