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 ,这对全局横切关注点有用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,543评论 25 707
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,468评论 6 13
  • 我不知道怎样把生活中的种种情愫转化为语言 心中默默淌过的...
    复合肥鸡阅读 224评论 1 1
  • 迷茫迷茫 在迷茫的日子里打发时间 无助无助 在无助的生活里追求光明 虚无虚无 在虚无的世界里浪费光阴 漫长漫长 ...
    BurningZone阅读 256评论 0 0