文档

§过滤器

Play 提供了一个简单的过滤器 API,用于将全局过滤器应用于每个请求。

§过滤器与动作组合

过滤器 API 旨在用于跨领域问题,这些问题会无差别地应用于所有路由。例如,以下是过滤器的常见用例

相反,动作组合 旨在用于路由特定的问题,例如身份验证和授权、缓存等等。如果您的过滤器不是您希望应用于每个路由的过滤器,请考虑使用动作组合,它更加强大。并且不要忘记,您可以创建自己的动作构建器,这些构建器将您自己定义的动作集组合到每个路由中,以最大限度地减少样板代码。

§一个简单的日志记录过滤器

以下是 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 是一个函数,它接受一个请求头并生成一个结果,第二个参数 requestHeader 是传入请求的实际请求头。

nextFilter 参数表示过滤器链中的下一个动作。调用它将导致调用该动作。在大多数情况下,您可能希望在将来的某个时间点调用它。如果您出于某种原因想要阻止请求,则可以决定不调用它。

在调用链中的下一个过滤器之前,我们保存了一个时间戳。调用下一个过滤器会返回一个Future[Result],它最终会被兑现。有关异步结果的更多详细信息,请查看处理异步结果章节。然后,我们通过调用带有闭包的map方法来操作Future中的Result,该闭包接受一个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懒加载值


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)
      }
    }
  }
}

这里的主要区别在于,除了创建一个新的 EssentialAction 来包装传入的 next 操作之外,当我们调用 next 时,我们会得到一个 Accumulator

您可以使用 through 方法将 Accumulator 与 Pekko Streams Flow 组合,并在需要时对流进行一些转换。然后,我们对迭代器的结果进行 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
    }
  }
}

虽然看起来有两个不同的过滤器 API,但实际上只有一个,即 EssentialFilter。早期示例中更简单的 Filter API 扩展了 EssentialFilter,并通过创建一个新的 EssentialAction 来实现它。传入的回调通过为 Result 创建一个 promise 来使其看起来跳过了主体解析,而主体解析和操作的其余部分是异步执行的。

下一步:测试您的应用程序


发现此文档中的错误?此页面的源代码可以在 这里 找到。在阅读了 文档指南 后,请随时贡献拉取请求。有任何问题或建议要分享?请访问 我们的社区论坛,与社区进行交流。