第十五章 中间件
在有些场合,需要对 Django 处理的每个 request 都执行某段代码。这类代码可能是在 view处理之前修改传入的 request,或者记录日志信息以便于调试,等等。
这类功能可以用 Django 的中间件框架来实现,该框架由切入到 Django 的 request/response处理过程中的钩子集合组成。这个轻量级低层次的 plug-in 系统,能用于全面的修改 Django的输入和输出。
每个中间件组件都用于某个特定的功能。如果顺序阅读这本书(谨对后现代主义者表示抱歉),你可能已经多次看到中间件了:
第 12 章中所有的 session 和 user 工具都籍由一小簇中间件实现(例如,由中间件设定
view 中可见的 request.session 和 request.user )。
第 13 章讨论的站点范围 cache 实际上也是由一个中间件实现,一旦该中间件发现与
view 相应的 response 已在缓存中,就不再调用对应的 view 函数。
第 14 章所介绍的 flatpages , redirects , 和 csrf 等应用也都是通过中间件组件来完成其魔法般的功能。
这一章将深入到中间件及其工作机制中,并阐述如何自行编写中间件。
什么是中间件
中间件组件是遵循特定 API 规则的简单 Python 类。在深入到该 API 规则的正式细节之前,先看一下下面这个非常简单的例子。
高流量的站点通常需要将 Django 部署在负载平衡 proxy(参见第 20 章)之后。这种方式将带来一些复杂性,其一就是每个 request 中的远程 IP 地址(request.META["REMOTE_IP"])将指向该负载平衡 proxy,而不是发起这个 request 的实际 IP。负载平衡 proxy 处理这个问题的方法在特殊的 X-Forwarded-For 中设置实际发起请求的 IP。
因此,需要一个小小的中间件来确保运行在 proxy 之后的站点也能够在
request.META["REMOTE_ADDR"] 中得到正确的 IP 地址:
class SetRemoteAddrFromForwardedFor(object): def process_request(self, request):
try:
real_ip = request.META['HTTP_X_FORWARDED_FOR'] except KeyError:
pass else:
# HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
# Take just the first one. real_ip = real_ip.split(",")[0]
request.META['REMOTE_ADDR'] = real_ip
一旦安装了该中间件(参见下一节),每个 request 中的 X-Forwarded-For 值都会被自动插入到 request.META['REMOTE_ADDR'] 中。这样,Django 应用就不需要关心自己是否位于负载平衡 proxy 之后;简单读取 request.META['REMOTE_ADDR'] 的方式在是否有 proxy 的情形下都将正常工作。
实际上,为针对这个非常常见的情形,Django 已将该中间件内置。它位于
django.middleware.http 中, 下一节将给出这个中间件相关的更多细节。
安装中间件
如果按顺序阅读本书,应当已经看到涉及到中间件安装的多个示例,因为前面章节的许多例子都需要某些特定的中间件。出于完整性考虑,下面介绍如何安装中间件。
要启用一个中间件,只需将其添加到配置模块的 MIDDLEWARE_CLASSES 元组中。在
MIDDLEWARE_CLASSES 中,中间件组件用字符串表示:指向中间件类名的完整 Python 路径。例如,下面是 django-admin.py startproject 创建的缺省 MIDDLEWARE_CLASSES :
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.doc.XViewMiddleware'
)
Django 项目的安装并不强制要求任何中间件,如果你愿意,MIDDLEWARE_CLASSES 可以为空。但我们建议启用 CommonMiddleware ,稍后做出解释。
这里中间件出现的顺序非常重要。在 request 和 view 的处理阶段,Django 按照
MIDDLEWARE_CLASSES 中出现的顺序来应用中间件,而在 response 和异常处理阶段,Django则按逆序来调用它们。也就是说,Django 将 MIDDLEWARE_CLASSES 视为 view 函数外层的顺序包装子:在 request 阶段按顺序从上到下穿过,而在 response 则反过来。关于 Django 处理阶段的详细信息,请参见第三章”Django 怎么处理一个请求: 完整细节”这一节。
中间件方法
现在,我们已经知道什么是中间件和怎么安装它,下面将介绍中间件类中可以定义的所有方法。
Initializer: init (self)
在中间件类中, __init () 方法用于执行系统范围的设置。
出于性能的考虑,每个已启用的中间件在每个服务器进程中只初始化 一 次。也就是说
__init () 仅在服务进程启动的时候调用,而在针对单个 request 处理时并不执行。
对一个 middleware 而言,定义 __init__() 方法的通常原因是检查自身的必要性。如果
__init () 抛出异常 django.core.exceptions.MiddlewareNotUsed ,则 Django 将从
middleware 栈中移出该 middleware。可以用这个机制来检查 middleware 依赖的软件是否存在、服务是否运行于调试模式、以及任何其它环境因素。
在中间件中定义 __init () 方法时,除了标准的 self 参数之外,不应定义任何其它参数。
Request 预处理函数: process_request(self, request)
这个方法的调用时机在 Django 接收到 request 之后,但仍未解析 URL 以确定应当运行的 view之前。Django 向它传入相应的 HttpRequest 对象,以便在方法中修改。
process_request() 应当返回 None 或 HttpResponse 对象.
如果返回 None , Django 将继续处理这个 request,执行后续的中间件, 然后调用相应的 view.
如果返回 HttpResponse 对象, Django 将不再执行 任何 其它的中间件(而无视其种类)以及相应的 view。 Django 将立即返回该 HttpResponse .
View 预处理函数: process_view(self, request, view, args, kwargs)
这个方法的调用时机在 Django 执行完 request 预处理函数并确定待执行的 view 之后,但在
view 函数实际执行之前。
表 15-1 列出了传入到这个 View 预处理函数的参数。
表 15-1. 传入 process_view()的参数 | |
参数 | 说明 |
request | HttpRequest 对象 . |
view | Django 将调用的处理 request 的 python 函数. 这是实际的函数对象本身, 而不是字符串表述的函数名。 |
args | 将传入 view 的位置参数列表,但不包括 request 参数(它通常是传 入 view 的第一个参数) |
kwargs | 将传入 view 的关键字参数字典. |
如同 process_request() , process_view() 应当返回 None 或 HttpResponse 对象。
如果返回 None , Django 将继续处理这个 request ,执行后续的中间件, 然后调用相应的 view.
如果返回 HttpResponse 对象, Django 将不再执行 任何 其它的中间件(不论种类)以及相应的 view. Django 将立即返回该 HttpResponse .
Response 后处理函数: process_response(self, request, response)
这个方法的调用时机在 Django 执行 view 函数并生成 response 之后。这里,该处理器就能修改 response 的内容;一个常见的用途是内容压缩,如 gzip 所请求的 HTML 页面。
这个方法的参数相当直观: request 是 request 对象,而 response 则是从 view 中返回的
response 对象。
不同可能返回 None 的 request 和 view 预处理函数, process_response() 必须 返回
HttpResponse 对象. 这个 response 对象可以是传入函数的那一个原始对象(通常已被修改),也可以是全新生成的。
Exception 后处理函数: process_exception(self, request, exception)
这个方法只有在request 处理过程中出了问题并且view 函数抛出了一个未捕获的异常时才会被调用。这个钩子可以用来发送错误通知,将现场相关信息输出到日志文件, 或者甚至尝试从错误中自动恢复。
这个函数的参数除了一贯的 request 对象之外,还包括 view 函数抛出的实际的异常对象
exception 。
process_exception() 应当返回 None 或 HttpResponse 对象.
如果返回 None , Django 将用框架内置的异常处理机制继续处理相应 request。
如果返回 HttpResponse 对象, Django 将使用该 response 对象,而短路框架内置的异常处理机制。
备注
Django 自带了相当数量的中间件类(将在随后章节介绍),它们都是相当好的范例。阅读这些代码将使你对中间件的强大有一个很好的认识。
在 Djangos wiki 上也可以找到大量的社区贡献的中间件范例: http://code.djangoproject.com/wiki/ContributedMiddleware
内置的中间件
Django 自带若干内置中间件以处理常见问题,将从下一节开始讨论。
认证支持中间件
中间件类: django.contrib.auth.middleware.AuthenticationMiddleware .
这个中间件激活认证支持功能. 它在每个传入的 HttpRequest 对象中添加代表当前登录用户的 request.user 属性。
完整的细节请参见第 12 章。
通用中间件
中间件类: django.middleware.common.CommonMiddleware .这个中间件为完美主义者提供了一些便利:
禁止 ``DISALLOWED_USER_AGENTS`` 列表中所设置的 user agent 访问 :一旦提供,这一列
表应当由已编译的正则表达式对象组成,这些对象用于匹配传入的 request 请求头中的 user-agent 域。下面这个例子来自某个配置文件片段:
import re DISALLOWED_USER_AGENTS = (
re.compile(r'^OmniExplorer_Bot'),
re.compile(r'^Googlebot')
)
请注意 import re ,因为 DISALLOWED_USER_AGENTS 要求其值为已编译的正则表达式(也就是
re.compile() 的返回值)。配置文件是常规的 python 文件,所以在其中包括 Python import语句不会有任何问题。
依据 ``APPEND_SLASH`` 和 ``PREPEND_WWW`` 的设置执行 URL 重写 :如果 APPEND_SLASH为 True , 那些尾部没有斜杠的 URL 将被重定向到添加了斜杠的相应 URL,除非 path 的最末组成部分包含点号。因此, foo.com/bar 会被重定向到 foo.com/bar/ , 但是
foo.com/bar/file.txt 将以不变形式通过。
如果 PREPEND_WWW 为 True , 那些缺少先导 www.的 URLs 将会被重定向到含有先导 www.的相应 URL 上。
这两个选项都是为了规范化 URL。其后的哲学是每个 URL 都应且只应当存在于一处。技术上 来说,URL example.com/bar 与 example.com/bar/ 及 www.example.com/bar/ 都互不相同。搜索引擎编目程序将把它们视为不同的 URL,这将不利于该站点的搜索引擎排名,因此这里的最佳实践是将 URL 规范化。
依据 ``USE_ETAGS`` 的设置处理 Etag : ETags 是 HTTP 级别上按条件缓存页面的优化机制。如果 USE_ETAGS 为 True ,Django 针对每个请求以 MD5 算法处理页面内容,从而得到 Etag,在此基础上,Django 将在适当情形下处理并返回 Not Modified 回应(译注:或者设置
response 头中的 Etag 域)。
请注意,还有一个条件化的 GET 中间件, 处理 Etags 并干得更多,下面马上就会提及。
压缩中间件
中间件类: django.middleware.gzip.GZipMiddleware .
这个中间件自动为能处理 gzip 压缩(包括所有的现代浏览器)的浏览器自动压缩返回]内容。这将极大地减少 Web 服务器所耗用的带宽。代价是压缩页面需要一些额外的处理时间。
相对于带宽,人们一般更青睐于速度,但是如果你的情形正好相反,尽可启用这个中间件。
条件化的 GET 中间件
中间件类: django.middleware.http.ConditionalGetMiddleware .
这个中间件对条件化 GET 操作提供支持。如果 response 头中包括 Last-Modified 或 ETag域,并且 request 头中包含 If-None-Match 或 If-Modified-Since 域,且两者一致,则该
response 将被 response 304(Not modified)取代。对 ETag 的支持依赖于 USE_ETAGS 配置及事先在 response 头中设置 ETag 域。稍前所讨论的通用中间件可用于设置 response 中的
ETag 域。
此外,它也将删除处理 HEAD request 时所生成的 response 中的任何内容,并在所有 request的 response 头中设置 Date 和 Content-Length 域。
反向代理支持 (X-Forwarded-For 中间件)
中间件类: django.middleware.http.SetRemoteAddrFromForwardedFor .这是我们在 什么是中间件 这一节中所举的例子。 在
request.META['HTTP_X_FORWARDED_FOR'] 存在的前提下,它根据其值来设置
request.META['REMOTE_ADDR'] 。在站点位于某个反向代理之后的、每个 request 的
REMOTE_ADDR 都被指向 127.0.0.1 的情形下,这一功能将非常有用。
红色警告!
这个 middleware 并 不 验证 HTTP_X_FORWARDED_FOR 的合法性。
如果站点并不位于自动设置 HTTP_X_FORWARDED_FOR 的反向代理之后,请不要使用这个中间件。否则,因为任何人都能够伪造 HTTP_X_FORWARDED_FOR 值,而 REMOTE_ADDR 又是依据
HTTP_X_FORWARDED_FOR 来设置,这就意味着任何人都能够伪造 IP 地址。
只有当能够绝对信任 HTTP_X_FORWARDED_FOR 值得时候才能够使用这个中间件。
会话支持中间件
中间件类: django.contrib.sessions.middleware.SessionMiddleware .这个中间件激活会话支持功能. 细节请参见第 12 章。
站点缓存中间件
中间件类: django.middleware.cache.CacheMiddleware .
这个中间件缓存 Django 处理的每个页面。已在第 13 章中详细讨论。
事务处理中间件
中间件类: django.middleware.transaction.TransactionMiddleware .
这个中间件将数据库的 COMMIT 或 ROLLBACK 绑定到 request/response 处理阶段。如果 view函数成功执行,则发出 COMMIT 指令。如果 view 函数抛出异常,则发出 ROLLBACK 指令。
这个中间件在栈中的顺序非常重要。其外层的中间件模块运行在 Django 缺省的 保存-提交行为模式下。而其内层中间件(在栈中的其后位置出现)将置于与 view 函数一致的事务机制的控制下。
关于数据库事务处理的更多信息,请参见附录 C。
iew 中间件
中间件类: django.middleware.doc.XViewMiddleware .
这个中间件将对来自 INTERNAL_IPS 所设置的内部 IP 的 HEAD 请求发送定制的 X-View HTTP头。Django 的自动文档系统使用了这个中间件。
下一章
Web 开发者和数据库模式设计人员并不总是享有白手起家打造项目的奢侈机会。下一章将阐述如何集成遗留系统,比如继承自 1980 年代的数据库模式。