Simple PlayFramework: HTTP Programming

Actions, Controllers and Results

Action是什么

Play应用中接收到的大多数请求都是由 Action 完成处理.

一个play.api.mvc.Action实质上是一个(play.api.mvc.Request => play.api.mvc.Result)函数,接收一个请求然后生成一个结果发送给客户端.

def echo = Action { request =>
  Ok("Got request [" + request + "]")
}

一个Action返回一个 play.api.mvc.Result值,作为一个HTTP响应发送回客户端,这个例子中,Ok构造了一个 200 OK的响应,并且包含了一个text/plain类型的响应体.

创建一个Action

play.api.mvc.Action的伴生对象提供了很多辅助方法用于创建一个Action值.

第一个最简单的例子就是,将一个表达式块作为参数,然后返回一个Result:

Action {
  Ok("Hello world")
}

这是创建Action最简单的方式,但是我们并没有引用传入的请求体.通常来说,存取调用这个Action的HTTP请求是非常常用的.

这是另一个Action Builder,将一个Request => Result函数作为参数:

Action { request =>
  Ok("Got request [" + request + "]")
}

或者经常将request标记为implicit,以便在其他需要的API中进行隐式的使用:

Action { implicit request =>
  Ok("Got request [" + request + "]")
}

最后一种创建方法是指定一个额外的BodyParser参数:

Action(parse.json) { implicit request =>
  Ok("Got request [" + request + "]")
}

BodyParser会在稍后进行解释,这里唯一需要知道的就是其他创建Action的方法都是使用一个默认的Any content body parser.

Controllers are action generators(控制器是Action的生成器)

Controller是一个用于生成Action值的生成器.Controller可以定义为一个类已收益与Dependency Injection(依赖注入),或者定义为一个Object.推荐的做法是定义为一个类,并且Object的方式是将来不会再进行支持.

这是一个最简单的用法来定义一个Action生成器,定义一个不就收参数的方法然后返回一个aciton值:

package controllers
import play.api.mvc._

// 定义一个Controller类,继承自Controller,然后根据创建Action的方法,在其中定义创建生成action值的方法
class Application extends Controller { 
  def index = Action {
    Ok("It works!")
  }
}

当然,一个action生成器方法也能够接收参数,然后这些参数可以在Action闭包中进行计算:

def hello(name: String) = Action {
  Ok("Hello " + name)
}

Simple results

现在我们只介绍简单的result: 一个HTTP结果,带有一个状态码,一组HTTP头,还有一个HTTP Body,这些将被发送给客户端.

这些Result都是使用play.api.mvc.Result进行定义:

import play.api.http.HttpEntity

def index = Action {
  Result(
    header = ResponseHeader(200, Map.empty),
    body = HttpEntity.Strict(ByteString("Hello world!"), Some("text/plain"))
  )
}

同时提供了一些辅助方法用于创建常用的Result,比如上面例子中的Okresult:

def index = Action {
  Ok("Hello world!")
}

这跟第一个例子创建的结果相同.

这是一些创建不同Result的例子:

val ok = Ok("Hello world!")
val notFound = NotFound
val pageNotFound = NotFound(<h1>Page not found</h1>)
val badRequest = BadRequest(views.html.form(formWithErrors))
val oops = InternalServerError("Oops")
val anyStatus = Status(488)("Strange response type")

所有这些辅助方法都可以在play.api.mvc.Results特质和它的半生对象中找到.

Redirects are simple results too(重定向也是简单的Result)

将浏览器重定向到一个新的URL也是一种简单的Result,只是这些result并不会携带响应体.

这是创建重定向Result的例子:

def index = Action {
  Redirect("/user/home")
}

默认使用303 SEE_OTHER作为响应类型,但是可以根据需要指定更多的状态码:

def index = Action {
  Redirect("/user/home", MOVED_PERMANENTLY)
}

或者可以将一个Result定义为 TODO来作为一个空的Result,会返回一个标准的Not implemented yet结果页.

HTTP routing

The built-in HTTP router

Router将每个传入的HTTP请求转换为一个Action.

Http请求在MV框架中已经见过,这个事件包含两个主要部分的信息:

  1. 请求路径,比如/clients/1542,包含了请求的字符串
  2. 请求方法,比如GET

Router在conf/routes文件中进行定义,是编译过的.这表示你可以直接在浏览器中看到路由错误的信息.

Dependency Injection(依赖注入)

Play支持创建两种类型的路由,一种是依赖注入路由,一种是静态路由.默认的是第一种,这也是Activator模板中展示的用例,因此推荐使用依赖注入路由.如果需要使用静态路由,可以在build.sbt文件中添加:

routesGenerator := StaticRoutesGenerator

该文档中的例子都假设将使用依赖注入路由生成器.

The routes file syntax(路由语法)

conf/routes文件是路由的配置文件.这个文件中列出了所有在应用中用到的路由.每个路由构成一个HTTP方法和一个URL模式,二者联合以调用一个Action生成器.

一个简单的例子:

GET   /clients/:id          controllers.Clients.show(id: Long)

每个路由以一个HTTP方法开始,跟随一个RUL模式,最后的部分是调用定义.

支持任何有效的HTTP方法,GET, PATCH, POST, PUT, DELETE, HEAD.

The URI pattern

URL模式定义了路由的请求路径,请求路径的部分可以使动态的.

静态路径

比如准确的匹配到GET /clients/all的请求,可以这样定义:

GET   /clients/all          controllers.Clients.list()

动态路由

如果想定义一个根据ID检索的路由,需要添加一个动态的部分:

GET   /clients/:id          controllers.Clients.show(id: Long)

支持多个动态的部分.

动态路由默认的匹配策略是正则表达式的[^/]+,表示任何动态部分被定义为:id都会精确的匹配一个URL部分.

Dynamic parts spanning several /

如果需要在动态部分计算多个URL的路径块,被斜杠分割,可以使用*id语法,会使用正则的.+语法:

GET   /files/*name          controllers.Application.download(name)

这会匹配比如GET /files/images/logo.png这样的URL,动态的name部分会计算images/logo.png的值.

Dynamic parts with custom regular expressions

同样可以自定义动态部分的正则表达式,使用$id<regex>语法:

GET   /items/$id<[0-9]+>    controllers.Items.show(id: Long)

Call to the Action generator method

这是路由定义的调用部分,这个部分必须有效的调用一个返回play.api.mvc.Action值的方法,通常作为控制器的action方法.

如果方法没有定义任何参数,只需要提供完整的方法名:

GET   /                     controllers.Application.homePage()

如果方法中定义了参数,所有这些参数会在请求的URI中查找,或者从URI路径中解析,或者从查询字符串.

# Extract the page parameter from the path.
GET   /:page                controllers.Application.show(page)

# Extract the page parameter from the query string.
GET   /                     controllers.Application.show(page)

这是在controllers.Application控制器中定义的对应的show方法:

def show(page: String) = Action {
  loadContentFromDatabase(page).map { htmlContent =>
    Ok(htmlContent).as("text/html")
  }.getOrElse(NotFound)
}

参数类型

对应String类型的参数,参数类型是可选提供的.如果需要将传入参数转换成指定类型,则可以指定需要的类型:

GET   /clients/:id          controllers.Clients.show(id: Long)

然后在show方法中进行同样的定义:

def show(id: Long) = Action {
  Client.findById(id).map { client =>
    Ok(views.html.Clients.display(client))
  }.getOrElse(NotFound)
}

Parameters with fixed values(带有固定值的参数)

有时候需要使用带有固定值的参数:

# Extract the page parameter from the path, or fix the value for /
GET   /                     controllers.Application.show(page = "home")
GET   /:page                controllers.Application.show(page)

带有默认值的参数

如果在传入的请求中没有解析到参数,可以提供一个默认值:

# Pagination links, like /clients?page=3
GET   /clients              controllers.Clients.list(page: Int ?= 1)

可选参数

可以指定一个可选参数,以便部分请求可以不需要提供该参数:

# The version parameter is optional. E.g. /api/list-all?version=3.0
GET   /api/list-all         controllers.Api.list(version: Option[String])

匹配优先级根据路由文件中的声明顺序.

Reverse routing(反向路由)

路由同样可以用于在Scala调用中生成一个URL,这使你讲所有的URL模式集中到一个配置文件称为可能,更有利于对应于的重构.

对于路由中使用的每个控制器,router会在routes模块生成一个reverse controller(反向控制器),用于同样的action方法和签名,但是返回一个play.api.mvc.Call而不是play.api.mvc.Action.

play.api.mvc.Call定义了一个HTTP调用,同时提供了HTTP方法和URI.

比如创建一个controller:

package controllers

import play.api._
import play.api.mvc._

class Application extends Controller {

  def hello(name: String) = Action {
    Ok("Hello " + name + "!")
  }

}

然后在conf/routes中做映射:

# Hello action
GET   /hello/:name          controllers.Application.hello(name)

然后就可以通过controllers.routes.Application翻转控制器,将URL翻转到action方法hello上:

// Redirect to /hello/Bob
def helloBob = Action {
  Redirect(routes.Application.hello("Bob"))
}

Manipulating Results(熟练操作Result)

修改默认的Content-Type

Result的类型根据你在响应体中指定的Scala值进行自动推断.比如:

val textResult = Ok("Hello World!")

这会自动设置Content-Typetext/plain,再比如:

val xmlResult = Ok(<message>Hello World!</message>)

会设置为application/xml.

这些是通过类型类play.api.http.ContentTypeOf设置的.

这些是非常有用的,但是有时候你需要对他进行修改.仅需要对result调用as(newContentType)方法就可以创建一个拥有不同Content-Type类似result:

val htmlResult = Ok(<h1>Hello World!</h1>).as("text/html")

或者更好的方式:

val htmlResult2 = Ok(<h1>Hello World!</h1>).as(HTML)

使用HTML的好处是会自动处理字符集,真正的Content-Type会被设置为text/html; charset=utf-8.

熟练操作HTTP头

同样可以添加或修改result的HTTP header设置:

val result = Ok("Hello World!").withHeaders(
  CACHE_CONTROL -> "max-age=3600",
  ETAG -> "xx")

注意在设置header属性的时候会自动覆盖掉result中原有header的相同键的属性.

设置或删除Cookies

Cookies是HTTP头的一个特殊表单,我们提供了一组辅助方法以便于操作.比如很简单的向一个HTTP响应添加header:

val result = Ok("Hello world").withCookies(Cookie("theme", "blue"))

或者将事先存储在浏览器中的Cookies删除:

val result2 = result.discardingCookies(DiscardingCookie("theme"))

或者在同一个响应体中添加或删除:

val result3 = result.withCookies(Cookie("theme", "blue")).discardingCookies(DiscardingCookie("skin"))

改变基于文本的HTTP响应体的字符集

对于基于文本的HTTP响应体,正确的处理字符集非常重要,Play中默认会使用utf-8处理.字符集用于将文本响应体转换成对应的字节以便在网络上进行传输,同时用于更新Content-Type头中;charset=xxx的部分.

字符集由类型类play.api.mvc.Codec自动处理,只需要在正确的作用域,隐式的引入一个play.api.mvc.Codec实例,就可以进行字符集的转换:

class Application extends Controller {

  implicit val myCustomCharset = Codec.javaSupported("iso-8859-1")

  def index = Action {
    Ok(<h1>Hello World!</h1>).as(HTML)
  }
}

这个例子中,作用域中有一个隐式的字符类型值,它会被用于Ok(...)方法以将XML消息转换为ISO-8859-1类型的字节,同时在Content-Type头中设置text/html; charset=iso-8859-1头.

如果你想了解上面推荐使用的HTML方法是如何工作的,下面是它的定义:

def HTML(implicit codec: Codec) = {
  "text/html; charset=" + codec.charset
}

可以根据这样的方式定义自己需要的字符集设置方法.

Session and Flash scopes

在Play中有何不同

如果想要跨越多个HTTP请求保存数据,可以将他们保存在Session或Flash域中.保存在Session中的数据对于整个用户Session都是可见的,而保存在Flash域中的数据仅仅是对下一个请求可见.

能够理解Session和Flash域的数据并不是存储在服务中而是添加到他们各自的后继请求上是非常重要的,使用cookies的机制.这表示数据量是很受限制的(4KB),并且只能存储字符串.默认的cookie名是PLAY_SESSION,可以在application.conf文件中通过session.cookieName进行设置.

如果cookie的名字发生变化了,则之前的cookie可以通过上面介绍的cookie销毁方法进行删除.

同时,cookie使用一个秘密的key进行标记,客户端不能进行修改,或者修改后会变成无效的cookie.

Play的Session并不推荐作为一个缓存使用,如果需要缓存一些指定Session相关的数据,可以使用Play内建的缓存机制,然后在用户Session中存储一个唯一的ID以对指定的用户进行关联.

默认情况下,Session并没有有效的超时机制,当用户关闭浏览器时会自动过期.如果需要一个有用的过期机制指定一个应用,需要在用户Session存储一个时间戳,然后根据你的应用进行使用.或者通过在application.conf文件中设置session.maxAge来指定session cookie的最大时间.

在Session中存储数据

由于Session只是一个Cookie,同样只是一个HTTPheader,可以像操作其他result属性一样来操作session数据.

Ok("Welcome!").withSession("connected" -> "user@gmail.com")

注意这将会替换整个Session,如果需要在一个已有的Session上添加元素,只需要将元素添加到传入的Session上,然后指定为新的Session:

Ok("Hello World!").withSession(request.session + ("saidHello" -> "yes"))

或者从传入的Session中移除元素:

Ok("Theme reset!").withSession(request.session - "theme")

读取Session值

可以从HTTP请求中检索传入的Session:

def index = Action { request =>
  request.session.get("connected").map { user =>
    Ok("Hello " + user)
  }.getOrElse {
    Unauthorized("Oops, you are not connected")
  }
}

丢弃整个Session

Ok("Bye").withNewSession

Flash scope

Flash域跟Session的工作方式不是完全相同,有两个不同的地方:

  1. 只为一个请求保存数据
  2. Flash Cookies是无标记的,使得用户可以对它进行修改

建议仅被用户在非Ajax应用中传送成功错误信息.

下面是一些使用Flash域的例子:

def index = Action { implicit request =>
  Ok {
    request.flash.get("success").getOrElse("Welcome!")
  }
}

def save = Action {
  Redirect("/home").flashing(
    "success" -> "The item has been created")
}

在视图中检索Flash域的值,添加一个隐式Flash参数:

@()(implicit flash: Flash)
...
@flash.get("success").getOrElse("Welcome!")
...

然后在Action中,像下面一样指定一个implicit request =>:

def index = Action { implicit request =>
  Ok(views.html.index())
}

Body parsers

What is a body parser?

一个HTTP请求是一个header跟随一个body,header非常小,可以安全的缓存在内存中,因此在Play中使用RequestHeader类处理.body可能会非常长,因此不会被缓存在内存中,使用stream进行处理.然后有些比较小的body同样可以在内存中处理,因此将body stream映射为内存中的一个对象,Play提供了一个BodyParser抽象.

由于Play是一个异步框架,传统的InputStream并不能读取请求体,因为输入流是阻塞的.当调用read时,调用它的线程必须等待数据可用.取而代之的是,Play使用一个叫做Reactive Stream的异步式流库.Akka Stream是它的一个实现,一个允许多种异步流API以类似的方式在一起公共的SPI,因此传统的InputStream基于的技术并不适合用于Play.

More about Actions

前面提到Action是一个Request => Result函数,这并不完全正确,然我们更加准确的观察Action特质:

trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}

首先看到一个泛型类型A,然后action中必须定义一个BodyParser[A].同时Request[A]的定义为:

trait Request[+A] extends RequestHeader {
  def body: A
}

A的类型是请求体的类型,我们可以使用任何Scala的类型作为请求体,比如String, NodeSeq, Array[Byte], JsonValue或者java.io.File,同时拥有一个能够处理他们的body parser.

一个Action[A]使用一个BodyParser[A]从HTTP请求中检索一个类型A的值,然后创建一个发送给action代码的Request[A]对象.

使用内建的body parser

大多数典型的web应用不需要自定义body parser,他们可以简单的使用Play内建的parser进行工作.这些parser包括的有,JSON,XML,表单(form),同样将简单的文本体作为字符串,字节体作为ByteString.

The default body parser

如果不指定选择一个parser,默认的parser会查看传入的Content-Type头,然后响应的去分析body.比如,Content-Typeapplication/json会被分析为JsValue,Content-Typeapplication/x-form-www-urlencoded被分析为Map[String, Seq[String]].

默认的parser会生成一个AnyContent类型的body,AnyContent支持的各种类型可以通过as方法进行访问,比如asJson,返回一个Option类型的body.

def save = Action { request =>
  val body: AnyContent = request.body
  val jsonBody: Option[JsValue] = body.asJson

  // Expecting json body
  jsonBody.map { json =>
    Ok("Got: " + (json \ "name").as[String])
  }.getOrElse {
    BadRequest("Expecting application/json request body")
  }
}

下面是默认parser支持的类型映射:

  1. text/plain: String,使用asText访问
  2. application/json: JsValue,使用asJson访问
  3. application/xml, text/xml,或,application/XXX+xml: scala.xml.NodeSeq,使用asXML访问
  4. application/form-url-encoded: Map[String, Seq[String]],使用asFormUrlEncoded访问
  5. multipart/form-data: MultipartFormData,使用asMultipartFormData访问
  6. 其他内容类型: RawBuffer,使用asRaw访问

由于性能的原因,默认的parser并不会去尝试解析一个 请求方法中并没有定义一个有意义body 的body,像HTTP协议中定义的那些.这表示它只解析POST, PUT, PATCH这些请求的body,而不去解析GET, HEAD, DELETE这些方法.如果需要解析这些方法的body,可以使用anyContentparser.

选择一个明确的body parser

如果需要明确的选择一个parser,可以将一个parser传入到Action的apply或async方法.

Play提供了很多parser,通过使用BodyParsers.parse对象,可以方便的通过Controller特质传入.

比如,定义一个处理JSON的action:

def save = Action(parse.json) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

注意这时body的类型为JsValue,而不再是一个Option,更易于进行处理.不再是一个Option的原因是,parser会验证传入的请求是否含有application/json类型的Content-Type,如果没有,则返回一个415 Unsupported Media Type响应.因此不需要在action代码里进行验证了.

这些流程意味着客户端会拥有更好的行为,在请求从传入正确的Content-Type头.如果想要处理的更轻松,可以使用tolerantJson,这会忽略Content-Type头并尝试以JSON方式分析body:

def save = Action(parse.tolerantJson) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

另一个例子,将请求体存储到文件:

def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
  Ok("Saved the request content to " + request.body)
}

组合body parser

上个例子中,所有的请求都被存储在同一个文件中,这种方式有点问题.让我们自定义一个parser,从请求Session中解析用户名,然后为每个用户创建一个单独的文件.

val storeInUserFile = parse.using { request =>
  request.session.get("username").map { user =>
    file(to = new File("/tmp/" + user + ".upload"))
  }.getOrElse {
    sys.error("You don't have the right to upload here")
  }
}

def save = Action(storeInUserFile) { request =>
  Ok("Saved the request content to " + request.body)
}

这里实质上并没有编写自己的parser,而是根据已有的parser进行了组合.但是能够覆盖到很多实用场景.

最大内容长度

基于文本的parser(text,json,xml,formUrlEncoded)会实用一个最大内容长度,因为他们需要把所有内容加载到内存.默认为100KB,可以通过在application.conf文件中指定play.http.parser.maxMemoryBuffer进行覆写:

play.http.parser.maxMemoryBuffer=128K

对于将内容混存到磁盘的parser,比如raw parsermultipart/form-data,通过play.http.parser.maxDiskBuffer属性进行设置,默认为10MB,multipart/form-dataparser同样强制数据字段聚合的文本最大长度属性.

同样可以覆写最大长度限制:

// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
  Ok("Got: " + text)
}

同样可以通过maxLength打包任何类型的parser:

// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request =>
  Ok("Saved the request content to " + request.body)
}

编写自定义body parser

可以通过实现BodyParser特质来自定义parser,这个特质实质上是一个简单的函数:

trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])

这个函数接收一个RequestHeader,用于检查请求的信息,通常是Content-Type属性.

函数返回值是一个Accumulator,一个Accumulator是一个Akka-streams Sink的薄的包装层.他将stream的元素异步集聚到一个结果,可以通过传入一个Akka-stream Source进行运行.当Accumulator完成时会返回一个Future.这本质上和Sink[E, Future[A]]是同一个东西.事实上它只不过是这个类型的包装器,但不同的是Accumulator提供了一些方便的方法用于操作这些作为promise的结果,比如map, mapFuture, recover等等.而Sink需要调用mapMaterializedValue来提供这些操作.

accumulator的apply方法返回ByteString类型的消耗元素,实质是字节数组.但与byte[]不同的是ByteString是不可变的,还有一些经常使用的操作如slice和append.

accumulator的返回类型为Either[Result, A],要么返回一个Result,要么是一个类型为A的body.A类型的结果通常是一个错误,比如body解析失败,或者Content-Type与parser接收的类型不匹配,或者内存缓存溢出.当parser返回一个结果时,将会对action的处理形成短路,parser的结果会立即被返回,action也不会再被调用.

Directing the body elsewhere(重定向body到别处)

编写parser常用的场景是比如你并不像真正解析一个body,或者你想将它导向别处,这可以定义一个parser:

import javax.inject._
import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.ws._
import scala.concurrent.ExecutionContext
import akka.util.ByteString

class MyController @Inject() (ws: WSClient)(implicit ec: ExecutionContext) {

  def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
    Accumulator.source[ByteString].mapFuture { source =>
      request
        // TODO: stream body when support is implemented
        // .withBody(source)
        .execute()
        .map(Right.apply)
    }
  }

  def myAction = Action(forward(ws.url("https://example.com"))) { req =>
    Ok("Uploaded")
  }
}

使用Akka streams自定义parser

有些稀有的场景下需要使用Akka streams来自定义parser.大多数情况下,首先会将body缓冲到一个ByteString,这通常会提供一个更简单的分析方法,因为可以使用命令方法或者对body随机访问.

然而当这些不可行的时候,比如需要处理的body太长而不能全部放在内存,这时需要编写一个自定义parser.

这是一个CSVparser,更详细的内容参考Akka-stream文档,或者Akka streams cookbook:

import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import akka.util.ByteString
import akka.stream.scaladsl._

val csv: BodyParser[Seq[Seq[String]]] = BodyParser { req =>

  // A flow that splits the stream into CSV lines
  val sink: Sink[ByteString, Future[Seq[Seq[String]]]] = Flow[ByteString]
    // We split by the new line character, allowing a maximum of 1000 characters per line
    .via(Framing.delimiter(ByteString("\n"), 1000, allowTruncation = true))
    // Turn each line to a String and split it by commas
    .map(_.utf8String.trim.split(",").toSeq)
    // Now we fold it into a list
    .toMat(Sink.fold(Seq.empty[Seq[String]])(_ :+ _))(Keep.right)

  // Convert the body to a Right either
  Accumulator(sink).map(Right.apply)
}

Action composition

Custom action builders

前面提过多种声明action的方法,带有请求参数的,不带请求参数的,或者带有body parser的.实时上比这还要多,将会在异步编程部分看到.

这些创建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)
  }
}

然后和使用Aciton的方式一样进行使用:

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

由于ActionBuilder提供了所有创建action的不同方法,这里也同样能够使用,比如声明一个body parser:

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

Composing actions

在很多应用中我们需要对个action创建器,一些用于不同类型的用户验证,一些用于提供不同类型的通用功能,等等.一些场景中,我们并不想为不同的action创建器重写logging actiong的代码,需要使用可重用的方法定义它.

可重用的actiong代码可以通过解压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类来创建action:

import play.api.mvc._

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

一个action可以使用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创建器混入解包action:

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

更多复杂的action

到现在为止我们只看到了不影响request的aciton,当然,我们可以读取并修改传入的request对象:

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"))
}

不同的请求类型

aciton部分允许你在HTTP层对请求或结果做额外的处理,通常你需要为上下文创建数据处理通道或者对request本身执行验证.ActionFunction可以被认为是request上的一个函数,将传入类型和输出类型参数化并传入下一层.每个action函数可以提供处理组件,比如用户验证,数据库对象查询,权限验证,或者其他你想在多个action中组合或复用的操作.

有一些预定义的特质实现了ActionFunction以用于不同类型的处理:

  1. ActionTransformer: 能够改变request,比如添加额外的信息
  2. ActionFilter: 能够选择性的拦截请求,比如在不改变请求值的情况下生成错误
  3. ActionRefiner: 上面两种情况的结合
  4. ActionBuilder: 一个特殊情况的功能,接收Request作为输入,然后创建aciton

用户验证

aciton函数的常用场景是用户验证,我们可以很容易的实现我们自己的用户验证action,通过转换将原始的request添加到一个新的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创建器.

为request添加信息

现在我们来看一个REST API为Item对象提供服务.在/item/:itemId路径下有很多路由,每个都需要查找Item.这种场景,将这个逻辑放到action函数会比较有用.

首先,创建一个request对象,将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),注意这个aciton在一个方法内定义并接收一个物品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)
  }
}

验证请求

最终我们想要验证一个请求是否能够继续,比如我们想检查UserAction中的用户是否有权限访问ItemAction中的物品,如果不能,在返回一个错误:

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开始):

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)
  }

Content negotiation

内容协商是一个用于将同一个资源(URI)提供不同的展示的机制.这非常有用,比如WEB服务中提供不同的合适支持(XML,JSON等).服务驱动协商实质上是使用Accept*请求头进行执行.

语言

可以同过play.api.mvc.RequestHeader#acceptLanguages获取请求接收的语言列表,通过Accept-Language头进行检索,根据他们的特性值进行排序.Play在play.api.mvc.Controller#lang方法中使用,为你的action提供一个隐式的play.api.i18n.Lang值,因此他们能够自动的使用最合适的语言(如果你的语言支持,或者应用默认使用的语言).

Content

类似的,play.api.mvc.RequestHeader#acceptedTypes方法提供一个请求的所有能接受的结果的MIME类型,通过Accept头检索,通过特性因素排序.

事实上,Accept并不真正的包含MIME类型,但是包含媒体范围(media ranges)(比如,请求就收所有text结果会设置’text/‘范围,`/*表示接受所有类型),控制器提供了一个高层的render`方法帮你处理这个媒体范围.比如下面的action定义:

val list = Action { implicit request =>
  val items = Item.findAll
  render {
    case Accepts.Html() => Ok(views.html.list(items))
    case Accepts.Json() => Ok(Json.toJson(items))
  }
}

Accepts.Html()Accepts.Json()作为解析器来测试提供的媒体范围是否能匹配text/htmlapplication/json.

请求解析器

查看play.api.mvc.AcceptExtractors.Accepts对象的API文档,获取Play在render方法支持的MIME类型列表.可以通过play.api.mvc.Acceptingcase类来为提供的MIME类型创建自己的解析器,比如下面的代码检查媒体范围是否能匹配audio/mp3MIME类型:

val AcceptsMp3 = Accepting("audio/mp3"){
    render {
        case AcceptsMp3() => ???
    }
}

错误处理

HTTP应用能够返回两种类型的错误,客户端错误或服务端错误.客户端错误表示已连接的客户端做了错误的处理,服务端错误表示服务端出现了错误.

Play在很多场景下自动发现客户端错误,这些错误包括难看的header值,不支持的内容类型,请求的资源无法找到.同样会在很多场景发现服务端错误,比如action代码抛出异常,Play会捕捉并生成一个服务端错误返回给客户端.

提供一个自定义错误处理器

可以在root包穿件一个ErrorHandler类来实现HttpErrorHandler,比如:

import play.api.http.HttpErrorHandler
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent._

class ErrorHandler extends HttpErrorHandler {

  def onClientError(request: RequestHeader, statusCode: Int, message: String) = {
    Future.successful(
      Status(statusCode)("A client error occurred: " + message)
    )
  }

  def onServerError(request: RequestHeader, exception: Throwable) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }
}

如果不想把错误处理器放在root包,或者想要根据不同的环境配置不同的处理器,可以在application.conf文件中定义play.http.errorHandler属性:

play.http.errorHandler = "com.example.ErrorHandler"

扩展默认的错误处理器

Play的默认错误处理器提供了很多有用的功能,比如在开发模式,当一个服务端错误发生时,Play会尝试指出并在页面渲染出异常的详细信息,以帮助你快速排查错误进行修复.或者你想在生产环境提供自定义的服务端错误处理,就像开发模式提供的功能一样,Play提供了一个DefaultHttpErrorHandler,你可以进行重写并混入自定义的逻辑.

import javax.inject._

import play.api.http.DefaultHttpErrorHandler
import play.api._
import play.api.mvc._
import play.api.mvc.Results._
import play.api.routing.Router
import scala.concurrent._

class ErrorHandler @Inject() (
    env: Environment,
    config: Configuration,
    sourceMapper: OptionalSourceMapper,
    router: Provider[Router]
  ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) {

  override def onProdServerError(request: RequestHeader, exception: UsefulException) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }

  override def onForbidden(request: RequestHeader, message: String) = {
    Future.successful(
      Forbidden("You're not allowed to access this resource.")
    )
  }
}