Action composition【翻译】


原文:Action composition

这章将介绍几种定义常用Action函数的方式。

自定义Action构建器

我们在前面看到过,有几种声明Action的方式——使用Request参数,不使用request参数,使用Body解析器等等。事实上,正如我们将在异步编程章节看到的. 还有更多的方式。

实际上,这些构建Action的方法都是通过一个叫 ActionBuilder 的特质定义的,并且我们用来声明我的Action的Action对象只是这个特质的实例。通过实现你自己的ActionBuilder, 你可以声明一个可被重复使用的Action栈,然后这可以被用来构建Action。

让我们从一个简单的日志装饰的例子开始,我想记录每次对这个Action的调用。第一种方式是在invokeBlock 方法中实现这个功能,这个方法通过ActionBuilder构建每个Action时调用:

import play.api.mvc._

object LoggingAction extends ActionBuilder[Request] {  
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {  
Logger.info("Calling action")  
block(request)  
}
}

现在我们可以用相同的方式使用 Action:

def index = LoggingAction {  
Ok("Hello World")  
}

由于 ActionBuilder 提供了构建Action的所有不同的方法,这也适用于,如声明一个自定义的Body解析器:

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

组成Action

在大多数应用里,我想要有多种Action的构建器,一些做不同类型的验证,一些提供不同类型的通用功能等等。这样的话,我们不想为每一个Action构建器类型重写我们的日志Action代码,我们想把它定义成一个可以服用的方式。

可复用Action代码可以通过封装Action被实现:

import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] {

def apply(request: Request[A]): Future[Result] = {  
Logger.info("Calling action")  
action(request)  
}

lazy val parser = action.parser  
}

我们也可以使用Action 构建器来构建Action而不用定义我们自己的Action类:

import play.api.mvc._

def logging[A](action: Action[A])= Action.async(action.parser) { request =>  
Logger.info("Calling action")  
action(request)  
}

可以使用composeAction 方法把Action混合进Action构建器:

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

现在构建者可以像前面那样被使用:

def index = LoggingAction {  
Ok("Hello World")  
}

我们也可以不使用Action构建器混入封装Action:

def index = Logging {  
Action {  
Ok("Hello World")  
}
}

更复杂的Action

到目前为止,我们已经展示了根本不会影响请求的Action。当然,我们也可以读和修改传入的请求对象:

import play.api.mvc._

def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>  
val newRequest = request.headers.get("X-Forwarded-For").map { xff =>  
new WrappedRequest[A](request) {  
override def remoteAddress = xff  
}
} getOrElse request
action(newRequest)  
}

注意:Play已经内置了对 X-Forwarded-For 头的支持

我们可以阻塞请求:

import play.api.mvc._

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._  
import play.api.libs.concurrent.Execution.Implicits._

def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>  
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))  
}

不同的请求类型

Action组合允许你在HTTP请求和响应级别上执行额外的处理,经常你想要构建数据转换的管道来添加上下文或者执行验证到请求自身。ActionFunction 可以被认为是做为请求的方法,参数化输入请求类型和输出请求类型传递个下一层。每一个Action方法可以表示模块化处理,如验证,数据库查找对象,权限检查,或者你希望跨Action组合和复用的其他的操作。
有几个实现了ActionFunction 的预定义的特质,他们对一些不同类型的操作有用:

  • ActionTransformer 可以改变请求,例如添加额外的信息。
  • ActionFilter 可以选择性了拦截请求,例如在不改变请求值的情况下产生错误。
  • ActionRefiner 是上述两种的一般情况。
  • ActionBuilder 使用Request 做为输入的函数的特例,因此可以构建Action。

你也可以通过实现invokeBlock方法定义你自己的想要的 ActionFunction。通常很容易让输入输出类型成为Request (使用WrappedRequest)的实例。

身份验证

Action方法最常用的案例之一是身份验证。我们可以很容易的实现我们自己的从原始的请求确定用户的身份验证的Action转换器,并把它添加到新的UserRequest中。注意由于他需要一个简单的Request 做为输入,因此这也是一个ActionBuilder:

import play.api.mvc._

class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)

object UserAction extends  
ActionBuilder[UserRequest] with ActionTransformer[Request, UserRequest] {  
def transform[A](request: Request[A]) = Future.successful {  
new UserRequest(request.session.get("username"), request)  
}
}

Play也提供了一个内置的身份验证的Action构建器。关于这个的信息和怎样使用它可以在这里找到.

注意:内置的身份验证Action构建器,只是一个在简单情况下实现身份验证的最少必要代码的便利的助手,它的实现和上面的例子类似。 如果你有比内置身份验证Action能满足的需求还要复杂的需求,那么推荐不只是简单的实现你自己的Action。

给请求增加信息

现在让我们考虑一下与Item类型的对象一起工作的REST API。在/item/:itemId 路径下也许有很多路由,他们中的每一个都要寻找Item。在这个案例中,把这个逻辑放到Action方法中也许是有用的。 首先,我们将创建一个增加了Item的请求对象到我们的UserRequest:

import play.api.mvc._

class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {  
def username = request.username  
}

现在我们将创建一个寻找那些Item的Action细化器,并返回一个错误(Left)Either 或者一个新的 ItemRequest (Right).注意这个Action细化器被定义在一个携带Item的ID的方法内:

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

验证请求

最后,我们也许想要一个验证请求是应该继续的Action方法。例如,也许我们想检查一下UserAction 的中的用户是否有权限从ItemAction获取Item,如果没有返回一个错误:

object PermissionCheckAction extends ActionFilter[ItemRequest] {  
def filter[A](input: ItemRequest[A]) = Future.successful {  
if (!input.item.accessibleByUser(input.username))  
Some(Forbidden)  
else  
None  
}
}

所有都放在一起

现在我们可以使用andThen 把这些Action方法连接到一起(以ActionBuilder开头)来创建一个Action:

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

Play也提供了一个全局过滤API ,这对全局横切关注点有用。

祝青

继续阅读此作者的更多文章