§フィルター
Play は、あらゆるリクエストに適用するグローバルフィルター向けの、シンプルなフィルター API を提供しています。
§フィルター vs アクション合成
フィルター API は、すべてのルートに無差別に適用される横断的な関心事を対象としています。フィルターの一般的なユースケースは、例えば以下のようなものです。
- ログ/測定値の収集
- GZIP エンコード
- 包括的なセキュリティフィルター
対照的に、アクション合成 は認証や認可、キャッシュなど、特定のルートに対する関心事を対象としています。もし、フィルターをすべてのルートに適用したいのでなければ、代わりにアクション合成の使用を検討してみてください。アクション合成はフィルターよりも遙かに強力です。また、定型的なコードを最小限にするために、ルート毎に独自の定義済みアクション群を構成する、アクションビルダーを作成できることも忘れないでください。
§シンプルなロギングフィルター
以下は、Play framework があるリクエストを処理するためにどれくらい時間が掛かったのか計測してロギングする、シンプルなフィルターです:
import play.api.mvc._
object LoggingFilter extends Filter {
def apply(next: (RequestHeader) => Result)(rh: RequestHeader) = {
val start = System.currentTimeMillis
def logTime(result: PlainResult): Result = {
val time = System.currentTimeMillis - start
Logger.info(s"${rh.method} ${rh.uri} took ${time}ms and returned ${result.header.status}"))
result.withHeaders("Request-Time" -> time.toString)
}
next(rh) match {
case plain: PlainResult => logTime(plain)
case async: AsyncResult => async.transform(logTime)
}
}
}
ここで何が起きているのか理解してみましょう。まず最初に気付くべき点は apply
メソッドのシグネチャです。これはカリー化された関数で、第一引数はリクエストヘッダーを受け取って結果を返す関数となる next
で、第二引数はリクエストヘッダーとなる rh
です。
next
引数は、フィルターチェーンにおける次のアクションを表現しています。これを実行すると、次に呼ばれるべきアクションが起動します。ほとんどの場合において、後続の処理のいくつかのタイミングで次のアクションを実行したくなることでしょう。何らかの理由により、リクエストをブロックしたいのであれば、次のアクションを実行しないと決断するのも良いかもしれません。
rh
引数は、このリクエストの実際のヘッダーです。
コードの次の部分は、リクエストのログを出力する関数です。この関数は PlainResult
を受け取り、リクエストの処理に掛かった時間をログに出力した後、レスポンスにこの Request-Time
を記録するヘッダを記録して返します。
最後に、次のアクションが実行され、このアクションが返す結果のパターンマッチが行われます。結果は PlainResult
または AsyncResult
のどちらかであり、AsyncResult
は最終的には PlainResult
として実行されます。いずれの場合においても logTime
関数が実行される必要がありますが、その実行方法はそれぞれ少しだけ異なっています。PlainResult
の場合、結果はすぐに利用できるので logTime
を直ちに実行します。一方、AsyncResult
の場合、結果はまだ利用できません。そのため logTime
関数は、PlainResult
が利用できるようになってから実行されるよう transform
メソッドに渡されます。
§簡易な文法
より簡易な文法でフィルタを宣言することができます:
val loggingFilter = Filter { (next, rh) =>
val start = System.currentTimeMillis
def logTime(result: PlainResult): Result = {
val time = System.currentTimeMillis - start
Logger.info(s"${rh.method} ${rh.uri} took ${time}ms and returned ${result.header.status}"))
result.withHeaders("Request-Time" -> time.toString)
}
next(rh) match {
case plain: PlainResult => logTime(plain)
case async: AsyncResult => async.transform(logTime)
}
}
これは val なので、なんらかのスコープ内においてのみ使用することができます。
§フィルターを使う
もっともシンプルにフィルターを使うには、Global
オブジェクトで WithFilters
トレイトを継承します:
import play.api.mvc._
object Global extends WithFilters(LoggingFilter, new GzipFilter()) {
...
}
手動でフィルターを実行することもできます:
import play.api._
object Global extends GlobalSettings {
override def doFilter(action: EssentialAction) = LoggingFilter(action)
}
§フィルターはどこに合う?
フィルターは、アクションがルーターによって見つけられた後に、そのアクションをラップします。これは、フィルターを使ってルーターに影響を与えるパスやメソッド、そしてクエリパラメーターを変換できないことを意味します。フィルターから別のアクションを実行することで、リクエストをそのアクションに移動させてしまうこともできますが、これをするとフィルターチェーンの残りをバイパスしてしまうことに気を付けてください。ルーターが実行される前にリクエストを変更する必要がある場合は、フィルターを使う代わりに、そのロジックを Global.onRouteRequest
に配置するのが、より良いやり方でしょう。
フィルターはルーティングが完了した後に適用されるので、RequestHeader
の tags
マップによってリクエストからルーティング情報にアクセスすることができます。例えば、アクションメソッドに対する実行時間をログに出力したいとします。この場合、logTime
を以下のように書き換えることができます:
def logTime(result: PlainResult): Result = {
val time = System.currentTimeMillis - start
val action = rh.tags(Routes.ROUTE_CONTROLLER) + "." + rh.tags(Routes.ROUTE_ACTION_METHOD)
Logger.info(s"${action} took ${time}ms and returned ${result.header.status}"))
result.withHeaders("Request-Time" -> time.toString)
}
ルーティングタグは Play ルーターの機能です。独自のルーターを使ったり、
Glodal.onRouteRequest
から独自のアクションを返す場合は、これらのパラメーターは利用できない場合があります。
§より強力なフィルター
Play は リクエストボディ全体にアクセスすることのできる、EssentialFilter
と呼ばれるより低レベルなフィルター API を提供しています。この API により、EssentialAction を他のアクションでラップすることができます。
上記のフィルター例を EssentialFilter
として書き直すと、以下のようになります:
import play.api.mvc._
object LoggingFilter extends EssentialFilter {
def apply(next: EssentialAction) = new EssentialAction {
def apply(rh: RequestHeader) = {
val start = System.currentTimeMillis
def logTime(result: PlainResult): Result = {
val time = System.currentTimeMillis - start
Logger.info(s"${rh.method} ${rh.uri} took ${time}ms and returned ${result.header.status}"))
result.withHeaders("Request-Time" -> time.toString)
}
next(rh).map {
case plain: PlainResult => logTime(plain)
case async: AsyncResult => async.transform(logTime)
}
}
}
}
next
に渡されたアクションをラップするために EssentialAction
を新しく作っている点は置いておくとして、ここでポイントとなる差異は next を実行すると Iteratee
が得られる点です。お望みであれば、Enumeratee
内においてこれをラップすることで、様々な変換を行うことができます。その後、シンプルなフォームにおいて行ったのと同じ方法で、この iteratee の結果を map
し、partial function と共に取り扱います。
異なるふたつのフィルター API が存在するように見えるかもしれませんが、存在するのはただひとつ、
EssentialFilter
だけです。先の例で登場したシンプルなFilter
API はEssentialFilter
を継承し、EssentialAction
を新しく作ることでこれを実装しています。引数に渡されたコールバックは、リクエストボディを解析して残りのアクションが非同期に実行されている間、Result
用の promise を作り、その結果をAsyncResult
として返すことで、リクエストボディの解析をスキップしているように見せています。