文档

§操作组合

本章介绍了几种定义通用操作功能的方法。

§自定义操作构建器

我们之前看到过,有多种方法可以声明操作 - 带有请求参数、没有请求参数、带有主体解析器等等。事实上,正如我们在关于异步编程的章节中将要看到的,还有更多方法。

这些用于构建操作的方法实际上都是由一个名为 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 的特征,它们对于不同类型的处理很有用。

您还可以通过实现 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() 访问已解析的请求主体。但是,在某些情况下,将主体解析推迟到通过操作组合定义的某些(或所有)操作处理完之后是有意义的。例如

当然,当延迟解析请求体时,在解析请求体之前执行的动作中,请求体将不会被解析,因此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)
      }
    )
  }
}

下一步:内容协商


发现此文档中的错误?此页面的源代码可以在此处找到。在阅读文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。