§フィルター
Playは、各リクエストにグローバルフィルターを適用するためのシンプルなフィルターAPIを提供します。
§フィルターとアクション合成の比較
フィルターAPIは、すべてのルートに無差別に適用される横断的な関心事のために設計されています。たとえば、フィルターの一般的なユースケースを以下に示します。
- ロギング/メトリクス収集
- GZIPエンコーディング
- セキュリティヘッダー
対照的に、アクション合成は、認証と認可、キャッシングなど、ルート固有の関心事のために設計されています。フィルターをすべてのルートに適用したくない場合は、代わりにアクション合成の使用を検討してください。アクション合成の方がはるかに強力です。また、独自のカスタム定義のアクションセットを各ルートに構成する独自のアクションビルダーを作成して、ボイラープレートを最小限に抑えることができることを忘れないでください。
§シンプルなロギングフィルター
以下は、Play Frameworkでリクエストの実行にかかる時間を計測してログに記録するシンプルなフィルターです。これはFilter
トレイトを実装しています。
import javax.inject.Inject
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.apache.pekko.stream.Materializer
import play.api.mvc._
import play.api.Logging
class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter with Logging {
def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
val startTime = System.currentTimeMillis
nextFilter(requestHeader).map { result =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
logger.info(
s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}"
)
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
ここで何が起こっているのかを理解しましょう。最初に気づくことは、apply
メソッドのシグネチャです。これはカリー化された関数で、最初のパラメータnextFilter
はリクエストヘッダーを受け取って結果を生成する関数であり、2番目のパラメータrequestHeader
は着信リクエストの実際のリクエストヘッダーです。
nextFilter
パラメータは、フィルターチェーンの次のアクションを表します。これを呼び出すと、アクションが呼び出されます。ほとんどの場合、将来のある時点でこれを呼び出すことになるでしょう。何らかの理由でリクエストをブロックしたい場合は、呼び出さないことを決定する場合があります。
チェーン内の次のフィルターを呼び出す前に、タイムスタンプを保存します。次のフィルターを呼び出すと、最終的に満たされるFuture[Result]
が返されます。非同期結果の詳細については、非同期結果の処理の章を参照してください。次に、Result
を受け取るクロージャでmap
メソッドを呼び出して、Future
内のResult
を操作します。リクエストにかかった時間を計算し、ログに記録し、result.withHeaders("Request-Time" -> requestTime.toString)
を呼び出してレスポンスヘッダーでクライアントに送り返します。
§フィルターの使用
フィルターを使用する最も簡単な方法は、ルートパッケージにHttpFilters
トレイトの実装を提供することです。Playのランタイム依存性注入サポート(Guiceなど)を使用している場合は、DefaultHttpFilters
クラスを拡張し、可変長引数のコンストラクタにフィルターを渡すことができます。
import javax.inject.Inject
import play.api.http.DefaultHttpFilters
import play.api.http.EnabledFilters
import play.filters.gzip.GzipFilter
class Filters @Inject() (
defaultFilters: EnabledFilters,
gzip: GzipFilter,
log: LoggingFilter
) extends DefaultHttpFilters(defaultFilters.filters :+ gzip :+ log: _*)
異なる環境で異なるフィルターを使用したい場合、またはこのクラスをルートパッケージに配置したくない場合は、application.conf
でplay.http.filters
をクラスの完全修飾名に設定することで、Playがクラスを検索する場所を設定できます。例えば
play.http.filters=com.example.MyFilters
コンパイル時依存性注入にBuiltInComponents
を使用している場合は、httpFilters
lazy valをオーバーライドするだけで済みます。
import play.api._
import play.filters.gzip._
import play.filters.HttpFiltersComponents
import router.Routes
class MyComponents(context: ApplicationLoader.Context)
extends BuiltInComponentsFromContext(context)
with HttpFiltersComponents
with GzipFilterComponents {
// implicit executionContext and materializer are defined in BuiltInComponents
lazy val loggingFilter: LoggingFilter = new LoggingFilter()
// gzipFilter is defined in GzipFilterComponents
override lazy val httpFilters = Seq(gzipFilter, loggingFilter)
lazy val router: Routes = new Routes( /* ... */ )
}
Playが提供するフィルターはすべて、BuiltInComponents
で動作するトレイトを提供しています。
- GzipFilterComponents
- CSRFComponents
- CORSComponents
- SecurityHeadersComponents
- AllowedHostsComponents
§フィルターはどこに適合するのか?
フィルターは、ルーターによってアクションが検索された後、アクションをラップします。これは、フィルターを使用してパス、メソッド、またはクエリパラメータを変換して、ルーターに影響を与えることができないことを意味します。ただし、フィルターから直接アクションを呼び出すことで、リクエストを別のアクションに転送できます。ただし、これによりフィルターチェーンの残りの部分がバイパスされることに注意してください。ルーターが呼び出される前にリクエストを変更する必要がある場合は、代わりに`HttpRequestHandler`にロジックを配置することをお勧めします。
フィルターはルーティングが完了した後に適用されるため、RequestHeader
のattrs
マップを介してリクエストからルーティング情報にアクセスできます。たとえば、アクションメソッドに対して時間をログに記録する場合があります。その場合、次のようにフィルターを更新することができます。
import javax.inject.Inject
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import org.apache.pekko.stream.Materializer
import play.api.mvc.Filter
import play.api.mvc.RequestHeader
import play.api.mvc.Result
import play.api.routing.HandlerDef
import play.api.routing.Router
import play.api.Logging
class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter with Logging {
def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
val startTime = System.currentTimeMillis
nextFilter(requestHeader).map { result =>
val handlerDef: HandlerDef = requestHeader.attrs(Router.Attrs.HandlerDef)
val action = handlerDef.controller + "." + handlerDef.method
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
logger.info(s"${action} took ${requestTime}ms and returned ${result.header.status}")
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
ルーティング属性は、Playルーターの機能です。カスタムルーターを使用する場合、またはカスタムリクエストハンドラーを介してカスタムアクションを返す場合、これらのパラメーターは使用できない場合があります。
§より強力なフィルター
Playは、リクエストの本文へのフルアクセスを可能にする、より低レベルのフィルターAPIであるEssentialFilter
を提供します。このAPIを使用すると、EssentialActionを別のアクションでラップできます。
上記フィルターの例をEssentialFilter
として書き直したものを次に示します。
import javax.inject.Inject
import scala.concurrent.ExecutionContext
import org.apache.pekko.util.ByteString
import play.api.libs.streams.Accumulator
import play.api.mvc._
import play.api.Logging
class LoggingFilter @Inject() (implicit ec: ExecutionContext) extends EssentialFilter with Logging {
def apply(nextFilter: EssentialAction): EssentialAction = new EssentialAction {
def apply(requestHeader: RequestHeader) = {
val startTime = System.currentTimeMillis
val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader)
accumulator.map { result =>
val endTime = System.currentTimeMillis
val requestTime = endTime - startTime
logger.info(
s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}"
)
result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}
}
ここで重要な違いは、渡されたnext
アクションをラップする新しいEssentialAction
を作成することとは別に、nextを呼び出すと、Accumulator
が返されることです。
必要に応じて、Accumulator
をPekko Streams Flowとthrough
メソッドを使用して構成し、ストリームへの変換を追加できます。その後、イテレータの結果をmap
し、処理します。
class AccumulatorFlowFilter @Inject() (actorSystem: ActorSystem)(implicit ec: ExecutionContext)
extends EssentialFilter {
private val logger = org.slf4j.LoggerFactory.getLogger("application.AccumulatorFlowFilter")
private implicit val logging: LoggingAdapter = Logging(actorSystem.eventStream, logger.getName)
override def apply(next: EssentialAction): EssentialAction = new EssentialAction {
override def apply(request: RequestHeader): Accumulator[ByteString, Result] = {
val accumulator: Accumulator[ByteString, Result] = next(request)
val flow: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString].log("byteflow")
val accumulatorWithResult = accumulator.through(flow).map { result =>
logger.info(s"The flow has completed and the result is $result")
result
}
accumulatorWithResult
}
}
}
2つの異なるフィルターAPIがあるように見えるかもしれませんが、APIは1つだけです。
EssentialFilter
。以前の例のよりシンプルなFilter
APIは、EssentialFilter
を拡張し、新しいEssentialAction
を作成して実装します。渡されたコールバックは、Result
のpromiseを作成することで本文の解析をスキップするように見えますが、本文の解析とアクションの残りの部分は非同期に実行されます。
次へ: アプリケーションのテスト
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントのガイドラインを読んだ後、プルリクエストを自由に投稿してください。質問や共有するアドバイスがありますか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。