Actions composition

这一章引入了几种定义通用action的方法

自定义action构造器

之前,我们已经介绍了几种声明一个action的方法 - 带有请求参数,无请求参数和带有body解析器(body parser)等。事实上还有其他一些方法,我们会在异步编程中介绍。

这些构造action的方法实际上都是有由一个命名为ActionBuilder的特性(trait)所定义的,而我们用来声明所有action的Action对象只不过是这个特性(trait)的一个实例。通过实现自己的ActionBuilder,你可以声明一些可重用的action栈,并以此来构建action。

让我们先来看一个简单的日志装饰器例子。在这个例子中,我们会记录每一次对该action的调用。

第一种方式是在invokeBlock方法中实现该功能,每个由ActionBuilder构建的action都会调用该方法:

  1. import play.api.mvc._
  2. object LoggingAction extends ActionBuilder[Request] {
  3. def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
  4. Logger.info("Calling action")
  5. block(request)
  6. }
  7. }

现在我们就可以像使用Action一样来使用它了:

  1. def index = LoggingAction {
  2. Ok("Hello World")
  3. }

ActionBuilder提供了其他几种构建action的方式,该方法同样适用于如声明一个自定义body解析器(body parser)等方法:

  1. def submit = LoggingAction(parse.text) { request =>
  2. Ok("Got a bory " + request.body.length + " bytes long")
  3. }

组合action

在大多数的应用中,我们会有多个action构造器,有些用来做各种类型的验证,有些则提供了多种通用功能等。这种情况下,我们不想为每个类型的action构造器都重写日志action,这时就需要定义一种可重用的方式。

可重用的action代码可以通过嵌套action来实现:

  1. import play.api.mvc._
  2. case class Logging[A](action: Action[A]) extends Action[A] {
  3. def apply(request: Request[A]): Future[Result] = {
  4. Logger.info("Calling action")
  5. action(request)
  6. }
  7. lazy val parser = action.parser
  8. }

我们也可以使用Action的action构造器来构建,这样就不需要定义我们自己的action类了:

  1. import play.api.mvc._
  2. def logging[A](action: Action[A]) = Action.async(action.parser) { request =>
  3. Logger.info("Calling action")
  4. action(request)
  5. }

Action同样可以使用composeAction方法混入(mix in)到action构造器中:

  1. object LoggingAction extends ActionBuilder[Request] {
  2. def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
  3. block(request)
  4. }
  5. override def composeAction[A](action: Action[A]) = new Logging(action)
  6. }

现在构造器就能像之前那样使用了:

  1. def index = LoggingAction {
  2. Ok("Hello World")
  3. }

我们也可以不用action构造器来混入(mix in)嵌套action:

  1. def index = Logging {
  2. Action {
  3. Ok("Hello World")
  4. }
  5. }

更多复杂的action

到现在为止,我们所演示的action都不会影响传入的请求。我们当然也可以读取并修改传入的请求对象:

  1. import play.api.mvc._
  2. def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
  3. val newRequest = request.headers.get("X-Forwarded-For").map { xff =>
  4. new WrappedRequest[A](request) {
  5. override def remoteAddress = xff
  6. }
  7. } getOrElse request
  8. action(newRequest)
  9. }
  1. 注意: Play已经内置了对X-Forwarded-For头的支持

我们可以阻塞一个请求:

  1. import play.api.mvc._
  2. def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
  3. request.headers.get("X-Forwarded-Proto").collect {
  4. case "https" => action(request)
  5. } getOrElse {
  6. Future.successful(Forbidden("Only HTTPS requests allowed"))
  7. }
  8. }

最后,我们还可以修改返回的结果:

  1. import play.api.mvc._
  2. import play.api.libs.concurrent.Execution.Implicits._
  3. def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
  4. action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
  5. }

不同的请求类型

当组合action允许在HTTP请求和响应的层面进行一些额外的操作时,你自然而然的就会想到构建数据转换的管道(pipeline),为请求本身增加上下文(context)或是执行一些验证。你可以把ActionFunction当做是一个应用在请求上的方法,该方法参数化了传入的请求类型和输出类型,并将其传至下一层。每个action方法可以是一个模块化的处理,如验证,数据库查询,权限检查,或是其他你想要在action中组合并重用的操作。

Play还有一些预定义的特性(trait),它们实现了ActionFunction,并且对不同类型的操作都非常有用:

  • ActionTransformer可以更改请求,比如添加一些额外的信息。
  • ActionFilter可选择性的拦截请求,比如在不改变请求的情况下处理错误。
  • ActionRefiner是以上两种的通用情况
  • ActionBuilder是一种特殊情况,它接受Request作为参数,所以可以用来构建action。

你可以通过实现invokeBlock方法来定义你自己的ActionFunction。通常为了方便,会定义输入和输出类型为Request(使用WrappedRequest),但这并不是必须的。

验证

Action方法最常见的用例之一就是验证。我们可以简单的实现自己的验证action转换器(transformer),从原始请求中获取用户信息并添加到UserRequest中。需要注意的是这同样也是一个ActionBuilder,因为其输入是一个Request

  1. import play.api.mvc._
  2. class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)
  3. object UserAction extends
  4. ActionBuilder[UserRequest] with ActionTransformer[Request, UserRequest] {
  5. def transform[A](request: Request[A]) = Future.successful {
  6. new UserRequest(request.session.get("username"), request)
  7. }
  8. }

Play提供了内置的验证action构造器。更多信息请参考这里

  1. 注意:内置的验证action构造器只是一个简便的helper,目的是为了用尽可能少的代码为一些简单的用例添加验证功能,其实现和上面的例子非常相似。
  2. 如果你有更复杂的需求,推荐实现你自己的验证action

为请求添加信息

现在让我们设想一个REST API,处理类型为Item的对象。在/item/:itemId的路径下可能有多个路由,并且每个都需要查询该item。这种情况下,将逻辑写在action方法中非常有用。

首先,我们需要创建一个请求对象,将Item添加到UserRequest中:

  1. import play.api.mvc._
  2. class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
  3. def username = request.username
  4. }

现在,创建一个action修改器(refiner)查找该item并返回Either一个错误(Left)或是一个新的ItemRequestRight)。注意这里的action修改器(refiner)定义在了一个方法中,用来获取该item的id:

  1. def ItemAction(itemId: String) = new ActionRefiner[UserRequest, ItemRequest] {
  2. def refine[A](input: UserRequest[A]) = Future.successful {
  3. ItemDao.findById(itemId)
  4. .map(new ItemRequest(_, input))
  5. .toRight(NotFound)
  6. }
  7. }

验证请求

最后,我们希望有个action方法能够验证是否继续处理该请求。例如,我们可能需要检查UserAction中获取的user是否有权限使用ItemAction中得到的item,如果不允许则返回一个错误:

  1. object PermissionCheckAction extends ActionFilter[ItemRequest] {
  2. def filter[A](input: ItemRequest[A]) = Future.successful {
  3. if (!input.item.accessibleByUser(input.username))
  4. Some(Forbidden)
  5. else
  6. None
  7. }
  8. }

合并起来

现在我们可以将所有这些action方法链起来(从ActionBuilder开始), 使用andThen来创建一个action:

  1. def tagItem(itemId: String, tag: String) =
  2. (UserAction andThen ItemAction(itemId) andThen PermissionCheckAction) { request =>
  3. request.item.addTag(tag)
  4. Ok("User " + request.username + " tagged " + request.item.id)
  5. }

Play同样支持全局过滤API,对于全局的过滤非常有用。