日記

日々のことと、Python/Django/PHP/Laravel/nodejs などソフトウェア開発のことを書き綴ります

Middleware

今日は Middleware です。

役割

ミドルウェアと言っても、Apache とか Tomcat とか、Oracleとかとは位置づけが異なっていて、Djangoミドルウェアというと、拡張ポイントのことを指しています。どの位置の拡張ポイントかというと、views.pyに処理が到達する以前の拡張ポイントです。JavaServlet APIで言うところの Filterの役割です。

実装方法

ミドルウェアは Context Processorとは違い、関数ではなくクラスで定義します。また、クラスの中の関数名と引数が次のように決められています。

process_request(self, request)

クライアントからアクセスがあって、URL から urls.py に定義された関数へのマッピングが行われる前に呼び出されます。つまり最前衛の関数。
型が無いのでわかりにくいですが、この関数は処理を継続するときは Noneを返します。
逆に不正なURLだとか、iPhoneしか許可しないぜ、っていうときには HttpResponseを返します。

process_view(self, request, view_func, view_args, view_kwargs)

この関数は process_request の次の段階です。ここの関数が呼び出される時点では、View関数が決まった状態になっています。それに伴って、呼び出し先のView関数(view_func)とそれの引数が渡されてきます。この関数の中で、View関数を呼ぶ必要はありません。
この関数でも次の処理に進む場合には、Noneを返します。
処理を中断したいときには、HttpResponseを返します。

process_response(self, request, response)

つづいて、これは View関数が呼び出された後に実行される関数です。
今度は responseが引数に追加されています。これは、View関数が返した HttpResponseです。
そしてこの関数だけは、必ず HttpResponseを返す必要があります。

process_exception(self, request, exception)

この関数は、View関数から投げられた例外を処理する関数です。
例外が発生したときだけ呼び出されます。
Noneを返すと処理は他のミドルウェアに委譲され、HttpResponseを返すとそれがクライアントに送信されます。

ミドルウェアを作ってみる

早速サンプルを作って試してみます。

app1/sample_middleware.py

# vim:fileencoding=utf-8
class EchoMiddleware:
    def process_request(self, request):
        print 'call process_request.'
        
        # Noneを返すと処理を継続して、HttpResponseを返すと
        # 処理を中断してクライアントにレスポンスを返します。
        return None
    
    def process_view(self, request, view_func, view_args, view_kwargs):
        print 'call process_view.'
        
        # Noneを返すと処理を継続して、HttpResponseを返すと
        # 処理を中断してクライアントにレスポンスを返します。
        return None
    
    def process_response(self, request, response):
        print 'call process_response.'
        
        # 必ず HttpResponse を返す。
        return response
    
    def process_exception(self, request, exception):
        print 'call process_exception.'
        
        # View が例外を創出した時の処理で
        # None か HttpResponse を返します。
        return None

各関数が呼び出されたことが分かるようにコメントを出すようにしてみました。
久々に first関数も、ちょっとだけ変更します。

app1/views.py

# vim:fileencoding=utf-8
from django.shortcuts import render_to_response

from django.http import HttpResponse
from django.template import loader
from django.template import RequestContext

def first(request):
    print 'call first.'
    from datetime import datetime
    
    t = loader.get_template('first.html')
    c = RequestContext(request, {
      'abc': u'abcですと!',
      'none_data': None,
      'empty_string': '',
      'two_value': 2,
      })
    return HttpResponse(t.render(c))

View関数もどこで呼び出されたかわかるようにコメントを追加しました。
これでコードの準備は完了ですが、Context Processorと同じように設定を追加する必要があります。

settings.py

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'app1.sample_middleware.EchoMiddleware',
)

MIDDLEWARE_CLASSESは、startprojectを実行したときに自動的に生成されています。上三つがデフォルトのミドルウェアということになります。ビルトインミドルウェアについては、マニュアルを参照しhttp://docs.djangoproject.com/en/1.1/ref/middleware/#ref-middlewareてください。

では、これをブラウザでアクセスして実行します。予想では、process_viewの後に first が来るはず。
今回は標準出力の結果を。。。

call process_request.
call process_view.
call first.
call process_response.

はい、予想通りこんな感じになりました。

続いて、views.pyを例外を投げるように修正します。

from django.shortcuts import render_to_response

from django.http import HttpResponse
from django.template import loader
from django.template import RequestContext

def first(request):
    print 'call first.'
    from datetime import datetime
    
    t = loader.get_template('first.html')
    c = RequestContext(request, {
      'abc': u'abcですと!',
      'none_data': None,
      'empty_string': '',
      'two_value': 2,
      })
    #return HttpResponse(t.render(c))
    raise Exception()

これを実行すると exception も標準出力に出てくるはず。

call process_request.
call process_view.
call first.
call process_exception.
call process_response.

はい、出てきましたね。
次に process_request関数を書き換えて、HttpResponseを返してみます。

    def process_request(self, request):
        print 'call process_request.'
        
        # Noneを返すと処理を継続して、HttpResponseを返すと
        # 処理を中断してクライアントにレスポンスを返します。
        return HttpResponse('hogehoge')

これを実行すると、process_requestだけが呼ばれて終わりそうですね。

call process_request.
call process_response.

お? process_responseも呼ばれています。process_responseはマニュアルには View関数の返した response だよと書いてあるようですが、実際にはミドルウェアがresponseを返しても全て、この関数を通るようです。

まとめ

MiddlewareがView関数の手前の拡張ポイントでした。Context ProcessorがContextの値を設定するだけなのに対して、ミドルウェアはクライアントからのリクエストの処理フローそのものを変更出来てしまうことに違いがあります。Context Processorも拡張ポイントですが、ミドルウェアはより低レイヤーな拡張ポイントと言えます。

その一方で、グローバルな設定にミドルウェアを書く必要があるため、全てのView関数が対象となってしまいます。次回はビルトインの Transaction Middlewareを参考にしながら、もう少し突っ込んだミドルウェアの使い方をしてみたいと思います。