§操作组合
本章介绍了几种定义通用操作功能的方法。
§自定义操作构建器
我们之前看到过,有多种方法可以声明操作 - 带有请求参数、没有请求参数、带有主体解析器等等。事实上,正如我们在关于异步编程的章节中将要看到的,还有更多方法。
这些用于构建操作的方法实际上都是由一个名为 ActionBuilder
的特质定义的,而我们用来声明操作的 Action
对象只是该特质的一个实例。通过实现您自己的 ActionBuilder
,您可以声明可重用的操作堆栈,然后可以使用这些堆栈来构建操作。
让我们从一个简单的日志记录装饰器示例开始,我们希望记录对该操作的每次调用。
第一种方法是在 invokeBlock
方法中实现此功能,该方法会为 ActionBuilder
构建的每个操作调用。
import play.api.mvc._
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser)
with Logging {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
logger.info("Calling action")
block(request)
}
}
现在,我们可以在控制器中使用 依赖注入 来获取 LoggingAction
的实例,并像使用 Action
一样使用它。
class MyController @Inject() (loggingAction: LoggingAction, cc: ControllerComponents)
extends AbstractController(cc) {
def index = loggingAction {
Ok("Hello World")
}
}
由于 ActionBuilder
提供了构建操作的所有不同方法,因此这也适用于例如声明自定义主体解析器。
def submit: Action[String] = loggingAction(parse.text) { request =>
Ok("Got a body " + request.body.length + " bytes long")
}
§组合操作
在大多数应用程序中,我们希望拥有多个操作构建器,一些执行不同类型的身份验证,一些提供不同类型的通用功能,等等。在这种情况下,我们不想为每种类型的操作构建器重写我们的日志记录操作代码,我们希望以可重用的方式定义它。
可重用的操作代码可以通过包装操作来实现。
import play.api.mvc._
case class Logging[A](action: Action[A]) extends Action[A] with play.api.Logging {
def apply(request: Request[A]): Future[Result] = {
logger.info("Calling action")
action(request)
}
override def parser = action.parser
override def executionContext = action.executionContext
}
我们也可以使用 Action
操作构建器来构建操作,而无需定义我们自己的操作类。
import play.api.mvc._
def logging[A](action: Action[A]) = Action.async(action.parser) { request =>
logger.info("Calling action")
action(request)
}
可以使用 composeAction
方法将操作混合到操作构建器中。
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
block(request)
}
override def composeAction[A](action: Action[A]): Logging[A] = new Logging(action)
}
现在,构建器可以像以前一样使用。
def index = loggingAction {
Ok("Hello World")
}
我们也可以在没有操作构建器的情况下混合包装操作。
def index = Logging {
Action {
Ok("Hello World")
}
}
§更复杂的操作
到目前为止,我们只展示了对请求没有任何影响的操作。当然,我们也可以读取和修改传入的请求对象。
import play.api.mvc._
import play.api.mvc.request.RemoteConnection
def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
val newRequest = request.headers.get("X-Forwarded-For") match {
case None => request
case Some(xff) =>
val xffConnection = RemoteConnection(xff, request.connection.secure, None)
request.withConnection(xffConnection)
}
action(newRequest)
}
注意:Play 已经内置支持
X-Forwarded-For
标头。
我们可以阻止请求。
import play.api.mvc._
import play.api.mvc.Results._
def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
request.headers
.get("X-Forwarded-Proto")
.collect {
case "https" => action(request)
}
.getOrElse {
Future.successful(Forbidden("Only HTTPS requests allowed"))
}
}
最后,我们还可以修改返回的结果。
import play.api.mvc._
def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}
§不同的请求类型
虽然操作组合允许您在 HTTP 请求和响应级别执行额外的处理,但通常您希望构建数据转换管道,这些管道为请求本身添加上下文或对其执行验证。ActionFunction
可以被认为是请求上的一个函数,它在输入请求类型和传递到下一层的输出类型上进行参数化。每个操作函数可能代表模块化处理,例如身份验证、对象数据库查找、权限检查或您希望在操作之间组合和重用的其他操作。
有一些预定义的实现 ActionFunction
的特征,它们对于不同类型的处理很有用。
ActionTransformer
可以更改请求,例如通过添加其他信息。ActionFilter
可以有选择地拦截请求,例如生成错误,而不会更改请求值。ActionRefiner
是上述两者的通用情况。ActionBuilder
是以Request
作为输入的函数的特例,因此可以构建操作。
您还可以通过实现 invokeBlock
方法来定义您自己的任意 ActionFunction
。通常,将输入和输出类型设置为 Request
的实例(使用 WrappedRequest
)很方便,但这并非严格必要。
§身份验证
操作函数最常见的用例之一是身份验证。我们可以轻松地实现自己的身份验证操作转换器,该转换器从原始请求中确定用户并将其添加到新的 UserRequest
中。请注意,这也是一个 ActionBuilder
,因为它以简单的 Request
作为输入
import play.api.mvc._
class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)
class UserAction @Inject() (val parser: BodyParsers.Default)(implicit val executionContext: ExecutionContext)
extends ActionBuilder[UserRequest, AnyContent]
with ActionTransformer[Request, UserRequest] {
def transform[A](request: Request[A]) = Future.successful {
new UserRequest(request.session.get("username"), request)
}
}
Play 还提供了一个内置的身份验证操作构建器。有关此构建器以及如何使用它的信息,请参阅 此处。
注意:内置的身份验证操作构建器只是一个方便的帮助程序,用于最大程度地减少实现简单情况身份验证所需的代码,其实现与上面的示例非常相似。
由于编写自己的身份验证帮助程序很简单,因此如果内置帮助程序不适合您的需求,我们建议您这样做。
§向请求添加信息
现在让我们考虑一个使用 Item
类型对象的 REST API。/item/:itemId
路径下可能存在许多路由,并且这些路由中的每一个都需要查找该项目。在这种情况下,将此逻辑放入操作函数中可能很有用。
首先,我们将创建一个请求对象,将 Item
添加到我们的 UserRequest
中
import play.api.mvc._
class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
def username = request.username
}
现在,我们将创建一个操作细化器,它查找该项目并返回 Either
错误 (Left
) 或新的 ItemRequest
(Right
)。请注意,此操作细化器是在一个接受项目 ID 的方法中定义的
def ItemAction(itemId: String)(implicit ec: ExecutionContext) = new ActionRefiner[UserRequest, ItemRequest] {
def executionContext = ec
def refine[A](input: UserRequest[A]): Future[Either[Status, ItemRequest[A]]] = Future.successful {
ItemDao
.findById(itemId)
.map(new ItemRequest(_, input))
.toRight(NotFound)
}
}
§验证请求
最后,我们可能需要一个操作函数来验证请求是否应该继续。例如,也许我们想检查 UserAction
中的用户是否有权访问 ItemAction
中的项目,如果没有,则返回错误
def PermissionCheckAction(implicit ec: ExecutionContext) = new ActionFilter[ItemRequest] {
def executionContext = ec
def filter[A](input: ItemRequest[A]) = Future.successful {
if (!input.item.accessibleByUser(input.username))
Some(Forbidden)
else
None
}
}
§将所有内容整合在一起
现在,我们可以使用 andThen
将这些操作函数链接在一起(从 ActionBuilder
开始),以创建一个操作
def tagItem(itemId: String, tag: String)(implicit ec: ExecutionContext): Action[AnyContent] =
userAction.andThen(ItemAction(itemId)).andThen(PermissionCheckAction) { request =>
request.item.addTag(tag)
Ok("User " + request.username + " tagged " + request.item.id)
}
Play 还提供了一个 全局过滤器 API ,它对于全局横切关注点很有用。
§与主体解析交互的操作组合
默认情况下,主体解析 在操作组合发生之前进行,这意味着您可以在每个操作中通过 request.body()
访问已解析的请求主体。但是,在某些情况下,将主体解析推迟到通过操作组合定义的某些(或所有)操作处理完之后是有意义的。例如
- 当您想通过 请求属性 将特定于请求的信息传递给主体解析器时。例如,用户依赖的最大文件上传大小或用户依赖的凭据,用于主体解析器应将上传重定向到的 Web 服务或对象存储。
- 当使用动作组合进行(粒度)授权时,您可能不想解析请求体,并在权限检查失败时尽早取消请求。
当然,当延迟解析请求体时,在解析请求体之前执行的动作中,请求体将不会被解析,因此request.body()
将返回null
。
您可以在conf/application.conf
中全局启用延迟解析请求体。
play.server.deferBodyParsing = true
请注意,与所有play.server.*
配置键一样,此配置在开发模式下不会被 Play 拾取,而只会在生产模式下拾取。要在开发模式下设置此配置,您必须在build.sbt
中设置它。
PlayKeys.devSettings += "play.server.deferBodyParsing" -> "true"
除了全局启用延迟解析请求体之外,您还可以使用路由修饰符deferBodyParsing
仅为特定路由启用它。
+ deferBodyParsing
POST / controllers.HomeController.uploadFileToS3
反之亦然。如果您全局启用延迟解析请求体,则可以使用路由修饰符dontDeferBodyParsing
为特定路由禁用它。
+ dontDeferBodyParsing
POST / controllers.HomeController.processUpload
现在可以通过调用play.api.mvc.BodyParser.parseBody
来解析请求体。
def home(): Action[AnyContent] = Action.async(parse.default) { implicit request: Request[AnyContent] =>
{
// When body parsing was deferred, the body is not parsed here yet, so following will be true:
// - request.body == null
// - request.attrs.contains(play.api.mvc.request.RequestAttrKey.DeferredBodyParsing)
// Do NOT rely on request.hasBody because it has nothing to do if a body was parsed or not!
BodyParser.parseBody(
parse.default,
request,
(req: Request[AnyContent]) => {
// The body is parsed here now, therefore:
// - request.body has a value now
// - request.attrs does not contain RequestAttrKey.DeferredBodyParsing anymore
Future.successful(Ok)
}
)
}
}
下一步:内容协商
发现此文档中的错误?此页面的源代码可以在此处找到。在阅读文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。