本章描述了常用定义Action的方法。
自定义action builders
我们在action一章已经看过如何声明一个action——有request parameter、无request parameter、有body parser等等。你可以在 asynchronous programming 一章中了解更多。
构建actions的方法实际上定义在 ActionBuilder 特质中,我们用来声明actions的Action对象实际上就是此特质的实例。通过实现自己的ActionBuilder 可以定义可重用的 action stacks,然后可以用它来构建actions。
让我们从一个简单的日志装饰器开始,我们将用它来记录单个访问此action的请求。
第一种方法是在invokeBlock方法中硬编码,它将被每个此actionBuilder创建的action调用:
import play.api.mvc._
class LoggingAction @Inject()(parser: BodyParsers.Default)(implicit ec: Executioncontext)
extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A] => Future[Result]) = {
Logger.info("Calling action")
block(request)
)
}
现在我们在controller中注入LogginAction的实例,和使用Action的方式一样:
class MyController @Inject()(loggingAction: LoggingAction, cc: ControllerComponents)
extends AbstractController(cc) {
def index = loggingAction {
Ok("Hello World")
}
}
由于ActionBuilder提供了所有构建actions的方法,因此也可以在这里声明一个自定义的body parser:
def submit = loggingAction(parse.text) { request =>
Ok("Got a body " + request.body.length + "bytes long")
}
组合actions
在大多数应用中我们都希望同时使用多个action builders,比如一些用来做认证校验,一些用来提供各种不同类型的通用方法。我们不希望为各个action builder来重写logging 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)
}
override def parser = action.parser
override def executinContext = action.executionContext
}
也可以通过使用 Action 这个action builder的默认实现来构建actions:
import play.api.mvc._
def logging[A](action: Action[A]) = Action.async(action.parser) { request =>
Logger.info("Calling action")
action(request)
}
可以通过composeAction方法来向其他 action builder 混入自己的Action:
class LoggingAction @Inject()(parser: BodyParer.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]) = new Logging(action)
}
现在我们可以像前面那样使用它了:
def index = loggingAction {
Ok("Hello World")
}
也可以在包装actions中混入,而不使用action builder:
def index = Logging {
Action {
Ok("Hello World")
}
}
更复杂的actions
目前为止我们展示的action还没有处理过request。我们可以如下操作它:
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的支持。
现在可以拦截request:
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"))
}
}
也可以用来修改要返回的result:
import play.api.mvc._
def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}
不同的request类型
action组合允许你在HTTP请求/响应级别上做附加操作,但是你经常需要构建数据传输管道,以便为请求添加上下文或者执行某些校验。ActionFunction是一个request上的函数,通过输入输出的请求类型进行参数化。单个的 action function 一般为独立的模块,如验证、数据库查询、许可校验,或是其它你希望在action中共享的操作。
这里是一些预定义的ActionFunction:
- ActionTransformer:用来修改request,如向request添加信息
- ActionFilter:可以选择性的拦截requset并产生错误信息,而不会影响request内容
- ActionRefiner:上面两个类的父类
- ActionBuilder:一个特殊的类,以request作为输入参数来创建action
可以通过实现invokeBlock方法来定义任意的ActionFunction。通常这是将输入输出类型转换为自定义Request类型的便捷方式(通过使用WrappedRequest),但是这不是严格要求的。
认证
一个action function的常见例子就是认证。可以简单的定义一个认证action,用来从原始request中抽取认证信息,并添加到新的UserRequest中 。请注意因为它以简单的Request作为输入参数,因此它仍然是一个ActionBuilder:
import play.api.mvc._
class UserRequest[A](val user: 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内置了一个action builder用于认证。移步这里查看详情。
注意:内置的认证action builder仅仅是一个非常简单的工具。我们建议实现自己的认证helper来应对你的认证请求。
向request添加信息
以 Item 类的REST API为例。类似 /item/:itemId 的路径将对item发起查询。让我们把业务逻辑放到action function中。
首先,向UserRequest中添加一个Item:
import play.api.mvc._
class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request){
def username = request.username
}
现在我们添加一个action refiner来查询Item,并返回一个Either表示一个错误或者新的ItemRequest。注意这个action refiner定义在方法中,可以直接拿到item的id:
def ItemAction(itemId: String)(implicit ec: ExecutionContext) = new ActionRefiner[UserRequest, ItemRequest]{
def executionContext = ec
def refine[A](input: UserRequest[A]) = Future.successful{
ItemDao.findById(itemId)
.map(new ItemRequest(_, input))
.toRight(NotFound)
}
}
校验requests
最后我们对request进行校验。如校验UserAction中的user是否有权限来查询ItemAction中的item:
def PermissionCheckAction(implicit ec: ExecutionContext) = new ActionFilter[ItemRequst]{
def executionContext = ec
def filter[A](input: ItemRequest[A]) = Future.successful{
if(!input.item.accessibleByUser(input.username))
Some(Forbidden)
else
None
}
}
合在一起
现在我们将它们串在一起(从ActionBuilder开始),使用andThen来创建action:
def tagItem(itemId: String, tag: String)(implicit ec: ExecutionContext) =
(userAction andThen ItemAction(itemId) andThen PermissionCheckAction) {request =>
request.item.addTag(tag)
Ok("User " + request.username + " tagged " + request.item.id)
}
最后,Play提供了一个全局的filter API,用来做全局的切面。