Action 组合
本章介绍几种定义通用action的方法
自定义action构造器
我们见过之前有多种声明一个action的方式 —— 带请求参数的,不带请求参数的,有请求体解析器的,等等。实际上还有更多的方法, 我们会在异步编程这一章中看到。
这些构造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
提供了所有构建actions的不同方式, 这同样适用于例如声明一个自定义的请求体解析器:
def submit = LoggingAction(parse.text) { request =>
Ok("Got a body " + request.body.length + " bytes long")
}
组合 actions
在多数应用程序中, 我们会想要多个action构造器, 有些用来做各种类型的身份验证, 有些提供各种类型的通用功能, 等等。这种情况下, 我们不想为每个类型的action构造器都重写日志action的代码, 就要定义一种可重用的方式。
可重用的action代码可以通过包装actions来实现:
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构造器来构建actions,这样就不有定义我们自己的action类了:
import play.api.mvc._
def logging[A](action: Action[A])= Action.async(action.parser) { request =>
Logger.info("Calling action")
action(request)
}
Actions可以使用composeAction
方法混入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构造器来混入包装的actions:
def index = Logging {
Action {
Ok("Hello World")
}
}
更多复杂的actions
到目前为止我们演示的actions都不影响传入的请求。当然, 我们也可以读取和修改传入的请求对象:
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参数作为输入, 从而可以构建actions。
你还可以通过实现invokeBlock
方法随意定义你自己的ActionFunction
。通常为了方便会让输入和输出类型为Request
(使用 WrappedRequest
), 但这不是必须的。
身份验证
Action函数最常见的用例之一就是身份验证。我们可以轻松实现我们自己的身份验证action变换器,从原始请求中获取用户信息,并添加到一个新的UserRequest
。要注意这同样也是一个ActionBuilder
,因为它带有一个简单的Request
作为输入:
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。
添加信息到请求
现在让我们考虑这样一个REST API,处理Item
类型的对象。在/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
}
现在我们创建一个action精炼器,查找该item并返回Either
一个错误(Left
) 或一个新的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
}
}
把他们合并在一起
现在我们将这些action函数链接在一起(从ActionBuilder
开始),使用andThen
来创建一个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 , 这对全局横切关注点非常有用。