随着Play 2.1的热销,很多人开始询问新的Play过滤器API。 实际上,API非常简单:
trait EssentialFilter {def apply(next: EssentialAction): EssentialAction
}
本质上,过滤器只是一个执行一个动作并返回另一个动作的函数。 过滤器通常会执行的操作是包装操作,并将其作为委托进行调用。 要将过滤器添加到应用程序中,只需将其添加到Global doFilter
方法中即可。 我们提供了一个帮助类来帮助您:
object Global extends WithFilters(MyFilter) {...
}
容易吧? 包装动作,在全局中进行注册。 好吧,这很容易,但前提是您了解Play架构。 这非常重要,因为一旦您了解了Play的体系结构,就可以使用Play进行更多的工作。 我们这里有一些文档,从较高的层次解释了Play的体系结构。 在这篇博客中,我将在过滤器的上下文中解释Play的体系结构,并附带代码片段和用例。
Plays架构简介
我不需要在这里进行深入介绍,因为我已经提供了指向我们的体系结构文档的链接,但是总而言之,Play的体系结构非常适合HTTP请求的流程。 发出HTTP请求时到达的第一件事是请求标头。 因此,Play中的动作必须是接受请求标头的函数。 HTTP请求中接下来会发生什么? 身体被接收。 因此,接收请求的函数必须返回消耗主体的东西。 这是一个迭代器,它是一个反应式流处理程序,在使用流后最终产生单个结果。 您不必为了了解过滤器而需要了解有关迭代操作方式的详细信息,需要了解的重要一点是,迭代最终会产生结果,您可以像使用将来那样使用其map
函数进行map
。 有关编写迭代对象的详细信息,请阅读我的博客文章 。 HTTP请求中发生的下一件事是必须发送http响应。 那么iteratee的结果是什么? HTTP响应。 HTTP响应是一组响应标头,后跟一个响应正文。 响应主体是一个枚举器,它是一个反应流生成器。 所有这些都是在Plays EssentialAction
特性中捕获的:
trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])
这表明基本动作是一个函数,该函数采用请求标头并返回迭代器,该迭代器消耗字节数组主体块并最终产生结果。
更简单的方法
在继续之前,我想指出Play提供了一个名为Filter
的辅助特性,它比使用EssentialFilter
时使编写过滤器更容易。 这类似于Action
特质,因为Action
无需担心迭代和如何解析主体,从而简化了编写EssentialAction
的过程,而只是提供了一个函数,该函数接受具有解析主体的请求,并返回结果。 Filter
特质以类似的方式简化了事情,但是我将一直讲到最后,因为我认为最好在开始使用助手类之前先了解过滤器的工作原理。
Noop过滤器
为了演示过滤器的外观,我将展示的第一件事是noop过滤器:
class NoopFilter extends EssentialFilter {def apply(next: EssentialAction) = new EssentialAction {def apply(request: RequestHeader) = {next(request)}}
}
每次执行过滤器时,我们都会创建一个包装它的新EssentialAction
。 由于EssentialAction
只是一个函数,我们可以调用它,传递传入的请求。 因此,以上是我们实现EssentialFilter
基本模式。
处理请求头
假设我们要查看请求标头,然后根据我们检查的内容有条件地调用包装的操作。 可以执行此操作的过滤器示例可能是网站/admin
区域的全面安全策略。 可能看起来像这样:
class AdminFilter extends EssentialFilter {def apply(next: EssentialAction) = new EssentialAction {def apply(request: RequestHeader) = {if (request.path.startsWith('/admin') && request.session.get('user').isEmpty) {Iteratee.ignore[Array[Byte]].map(_ => Results.Forbidden())} else {next(request)}}}
}
您可以在此处看到,由于我们是在解析正文之前拦截动作,因此在阻止动作时我们仍然需要提供一个正文解析器。 在这种情况下,我们将返回一个将忽略整个正文的正文解析器,并将其映射为禁止的结果。
处理身体
在某些情况下,您可能想对过滤器中的主体进行处理。 在某些情况下,您可能想解析正文。 如果是这种情况,请考虑改用动作合成 ,因为这样可以在动作解析正文之后挂接到动作处理。 如果要在过滤器级别解析主体,则必须对其进行缓冲,解析,然后再次对其进行流传输以使动作再次解析。 但是,有些事情可以在过滤器级别轻松完成。 一个示例是gzip解压缩。 Play框架已经提供了开箱即用的gzip解压缩功能,但是如果没有的话,它可能是这样(使用我的play extra iteratees项目中的gunzip枚举):
class GunzipFilter extends EssentialFilter {def apply(next: EssentialAction) = new EssentialAction {def apply(request: RequestHeader) = {if (request.headers.get('Content-Encoding').exists(_ == 'gzip')) {Gzip.gunzip() &>> next(request)} else {next(request)}}}
}
在这里,我们使用iteratee组成将人体分析器iteratee包裹在gunzip枚举对象中。
处理响应头
当您进行过滤时,您通常会希望对正在发送的响应进行处理。 如果您只想添加标题,或向会话中添加内容,或对响应进行任何写操作,而无需实际读取它,那么这很简单。 例如,假设您要向每个响应添加一个自定义标头:
class SosFilter extends EssentialFilter {def apply(next: EssentialAction) = new EssentialAction {def apply(request: RequestHeader) = {next(request).map(result => result.withHeaders('X-Sos-Message' -> 'I'm trapped inside Play Framework please send help'))}}
}
使用处理身体的iteratee上的map
函数,我们可以访问该动作产生的结果,然后可以按照演示进行修改。 但是,如果您想读取结果,则需要解开包装。 播放结果是AsyncResult
或PlainResult
。 一个AsyncResult
是一个Result
,其中包含一个Future[Result]
。 它具有允许最终的PlainResult
transform
方法。 PlainResult
具有标题和正文。 因此,假设您要向每个新创建的会话添加时间戳以记录创建时间。 可以这样完成:
class SessionTimestampFilter extends EssentialFilter {def apply(next: EssentialAction) = new EssentialAction {def apply(request: RequestHeader) = {def addTimestamp(result: PlainResult): Result = {val session = Session.decodeFromCookie(Cookies(result.header.headers.get(HeaderNames.COOKIE)).get(Session.COOKIE_NAME))if (!session.isEmpty) {result.withSession(session + ('timestamp' -> System.currentTimeMillis.toString))} else {result}}next(request).map {case plain: PlainResult => addTimestamp(plain)case async: AsyncResult => async.transform(addTimestamp)}}}
}
处理响应主体
您可能要做的最后一件事是转换响应主体。 PlainResult
有两个实现, SimpleResult
(用于没有传输编码的主体)和ChunkedResult
(用于分块传输编码的主体)。 SimpleResult
包含一个枚举器,而ChunkedResult
包含一个接受迭代器以将结果写出的函数。 您可能要执行的操作示例是实现gzip过滤器。 一个非常幼稚的实现(例如,不要使用它,而是使用我的play iteratees项目中的完整实现),如下所示:
class GzipFilter extends EssentialFilter {def apply(next: EssentialAction) = new EssentialAction {def apply(request: RequestHeader) = {def gzipResult(result: PlainResult): Result = result match {case simple @ SimpleResult(header, content) => SimpleResult(header.copy(headers = (header.headers - 'Content-Length') + ('Content-Encoding' -> 'gzip')), content &> Enumeratee.map(a => simple.writeable.transform(a)) &> Gzip.gzip())}next(request).map {case plain: PlainResult => gzipResult(plain)case async: AsyncResult => async.transform(gzipResult)}}}
}
使用更简单的API
现在,您已经了解了如何使用基本的EssentialFilter
API来实现所有目标,并希望因此了解了过滤器如何适应Play的体系结构以及如何利用它们来满足您的要求。 现在让我们看一下更简单的API:
trait Filter extends EssentialFilter {def apply(f: RequestHeader => Result)(rh: RequestHeader): Resultdef apply(next: EssentialAction): EssentialAction = {...}
}object Filter {def apply(filter: (RequestHeader => Result, RequestHeader) => Result): Filter = new Filter {def apply(f: RequestHeader => Result)(rh: RequestHeader): Result = filter(f,rh)}
}
简而言之,该API允许您编写过滤器而不必担心正文解析器。 它看起来好像动作只是结果的请求标头的功能。 这限制了过滤器的全部功能,但是对于许多用例而言,您根本不需要此功能,因此使用此API提供了一种简单的替代方法。 为了演示,noop过滤器类如下所示:
class NoopFilter extends Filter {def apply(f: (RequestHeader) => Result)(rh: RequestHeader) = {f(rh)}
}
或者,使用Filter
随播对象:
val noopFilter = Filter { (next, req) =>next(req)
}
请求计时过滤器可能如下所示:
val timingFilter = Filter { (next, req) =>val start = System.currentTimeMillisdef logTime(result: PlainResult): Result = {Logger.info('Request took ' + (System.currentTimeMillis - start))result}next(req) match {case plain: PlainResult => logTime(plain)case async: AsyncResult => async.transform(logTime)}
}
参考: James and Beth Roper的博客博客中的JCG合作伙伴 James Roper 了解了Play Filter API 。
翻译自: https://www.javacodegeeks.com/2013/02/understanding-the-play-filter-api.html