去年项目重构,我被中间件的调用顺序折磨得够呛。各种第三方中间件堆在一起,改一个就崩一片。直到我翻开 Django 源码,才明白自己一直在瞎写。
Django 的中间件是一层套一层的。请求进来时,从上往下走。响应返回时,从下往上走。这个模式叫洋葱圈。每个中间件都能在请求到达视图前做拦截,也能在响应返回客户端前做处理。
我以前以为中间件就是简单的顺序执行。看了源码才发现,request 进来时走 process_request,response 回去时走 process_response。这两个方向是相反的。写中间件时,必须想清楚代码要放在哪个方法里。
举个例子。你有个日志中间件,想记录请求耗时。在 process_request 里记个开始时间,怎么传给 process_response?答案是用 request 对象。给 request 绑定一个属性就行。request._start_time = time.time(),然后在 process_response 里读这个值。这个技巧在 Django 源码里随处可见。
还有一个坑是顺序问题。Django 的中间件列表从上往下是请求的顺序,但返回的顺序是反过来的。你要是把安全相关的中间件放在了后面,那前面的中间件就可能暴露敏感信息。
我翻 confirm MiddlewareMixin 的代码时,发现一个细节。Django 会把中间件列表里每个元素都转成能调用的对象。这个过程很短,但很有价值。它说明中间件不需要继承特定基类,只要有相应的方法就行。这给了我们自由度。
查看源码后,我最深的感受是:中间件不是一个黑盒子。你可以完全控制请求和响应的每个环节。比如实现一个限流中间件,只要在 process_request 里检查 IP 的访问频率,频率超了就返回 429 状态码。这样就不会执行后面的中间件和视图函数,直接返回响应。
还有一个点让我印象深刻。Django 的中间件处理异常时,用的是 process_exception。它只在 process_request 或者视图函数抛出异常时触发。process_response 里抛异常不会被捕获。这个细节在实际开发中经常被忽略。
我现在的做法是:每个中间件只做一件事。记录日志的只记录日志,做权限的只做权限,跨域处理的只处理跨域。这样出了问题,我能马上定位到是哪个中间件。
写中间件时,回掉函数不要有任何副作用。不能改变传入的 request 对象的结构,除非你清楚自己在做什么。因为其他中间件可能依赖这个结构。
看源码的过程中,我还注意到 Django 的中间件是惰性加载的。只有在请求到达时才会去实例化。这个设计很聪明,避免了无用的对象创建。
最后分享一个实用的例子。我需要在一个项目里给所有 API 响应加一个版本号。写了个中间件:process_request 里不做任何事,process_response 里把版本信息加到响应头里。代码只有四行,干净利落。
这个版本号中间件,让我真正体会到了中间件的威力。你不用修改视图函数,不用改动路由,不用侵入业务代码。在请求和响应的道路上,放一个人去处理就行了。
把你需要全局处理的逻辑都放到中间件里去吧。这样你的视图函数会干净得像刚洗完的碟子。
夜雨聆风