§过滤器
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 来使其看起来跳过了主体解析,而主体解析和操作的其余部分是异步执行的。
下一步:测试您的应用程序
发现此文档中的错误?此页面的源代码可以在 这里 找到。在阅读了 文档指南 后,请随时贡献拉取请求。有任何问题或建议要分享?请访问 我们的社区论坛,与社区进行交流。