Django 中间件常被形容成"洋葱模型"——请求从外到内穿过每一层,响应再从内到外穿回去。这一篇本机真实验证了这个模型,顺着源码找到"洋葱"是怎么被真实拼装出来的(一个出人意料地简单的 reversed() 循环),还真实复现了一个因为中间件顺序写反而触发的真实报错——证明这个顺序真的不是随便排的。
写三个最简单的中间件,各自在请求阶段和响应阶段打一行日志,挂到 MIDDLEWARE = [A, B, C],真实发一个请求。
def make_middleware(name):
def middleware(get_response):
def mw(request):
print(f' -> entering {name} (request phase)')
response = get_response(request)
print(f' <- leaving {name} (response phase)')
return response
return mw
return middleware
# 挂载为 MIDDLEWARE = ['mw_test.MwA', 'mw_test.MwB', 'mw_test.MwC']
-> entering MwA (request phase)
-> entering MwB (request phase)
-> entering MwC (request phase)
** actual view executing **
<- leaving MwC (response phase)
<- leaving MwB (response phase)
<- leaving MwA (response phase)
请求阶段严格按 MIDDLEWARE 列表里写的顺序(A→B→C)进去,响应阶段严格反过来(C→B→A)出来——写在最前面的中间件,最先碰到请求,也最后碰到响应。
拼装过程出人意料地朴素——就是一个 reversed() 循环。
django/core/handlers/base.py · django @ 6.0.7, L26 def load_middleware(self, is_async=False): handler = convert_exception_to_response(get_response) # 最内层:真正处理视图 for middleware_path in reversed(settings.MIDDLEWARE): # 注意:倒着遍历! middleware = import_string(middleware_path) mw_instance = middleware(adapted_handler) # 把"当前 handler"包起来 ... handler = convert_exception_to_response(mw_instance) # 新的 handler 换成刚包好的这层 # 循环结束,handler 就是最外层——从最后一个中间件开始包, # 包到第一个中间件结束,第一个自然就成了最外层
关键在 reversed(settings.MIDDLEWARE):从列表最后一个开始包。第一次循环,C 把"视图本身"包起来,变成新的 handler;第二次循环,B 把"刚包好的 C"包起来;最后一次循环,A 把"已经包了 B、C 的东西"包起来。循环结束后,最外层自然就是列表里写在最前面的那个——这就是"洋葱"的字面构造过程,不是什么特殊算法,就是从后往前一层层套娃。
如果中间那层中间件直接返回响应、根本不调用 get_response(),里面的层和视图确实会被跳过——但外层呢?
# MIDDLEWARE = [A, Block, C], Block 直接返回响应不调用 get_response-> entering MwA (request phase) -> entering MwBlock (request phase) !! MwBlock returns EARLY, get_response() never called, inner layers and view SKIPPED <- leaving MwA (response phase) # 注意:MwC 和视图确实完全没跑,但 MwA 的 "leaving" 那行照样打印了
MwC 和视图确实一次都没执行,但 MwA 的响应阶段代码照样跑了——因为短路只是"不往更里层传",并不会让已经在调用栈上的外层代码消失。MwA 在短路发生前已经调用了 get_response(request),这个调用迟早要返回(不管里面发生了什么),返回之后 MwA 自己的收尾代码自然会继续执行。
Django 自带的 AuthenticationMiddleware 依赖 SessionMiddleware 先跑——如果顺序反了会怎样?
django/contrib/auth/middleware.py · django @ 6.0.7, L30 class AuthenticationMiddleware(MiddlewareMixin): def process_request(self, request): if not hasattr(request, "session"): raise ImproperlyConfigured( "The Django authentication middleware requires session " "middleware to be installed. Edit your MIDDLEWARE setting to " "insert 'django.contrib.sessions.middleware.SessionMiddleware' " "before 'django.contrib.auth.middleware.AuthenticationMiddleware'." ) request.user = SimpleLazyObject(lambda: get_user(request))
# 故意写反顺序: # MIDDLEWARE = ['...auth.AuthenticationMiddleware', '...sessions.SessionMiddleware']django.core.exceptions.ImproperlyConfigured: The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE setting to insert 'django.contrib.sessions.middleware. SessionMiddleware' before 'django.contrib.auth.middleware. AuthenticationMiddleware'.
报错信息和源码里的字符串一字不差。原因直接对应上一节的洋葱模型:AuthenticationMiddleware 排在前面 = 更外层 = 请求阶段更早执行,如果它比 SessionMiddleware 先跑,这时候 request.session 还没被设置上,hasattr 检查自然失败。django-admin startproject 生成的默认 MIDDLEWARE 列表里,SessionMiddleware 排第二、AuthenticationMiddleware 排第五,顺序是真的经过设计的,不是随手写的。
先看完整走一遍(A→B→C→视图→C→B→A),再看 B 直接短路时会发生什么。
数据来自独立验证过的 Python 模拟——用真实的"倒序包装"逻辑复现洋葱构造,两种场景(完整通过、B 短路)都用独立的调用栈回放算法核对过:每一层的"进入"和"离开"事件严格按后进先出配对,短路场景里 C 和视图确实一次都没被记录到。