Play Framework 参考手册

目录:

第一章 Actions、Controllers与Results

什么是Action

Play应用中大多数的请求都是通过 Action 来处理。 Action 本质上是一个函数, (play.api.mvc.Request => play.api.mvc.Result) ,它接收请求,处理之后再响应客户端。

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

创建Action

最简单的方式:

Action {
  Ok("Hello world")
}

接收请求数据:

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

指定 Bodyparse 参数:

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

Controllers是Action生成器

Controllers 用于生成 Action

package controllers

import play.api.mvc._

class Application extends Controller {

  def index = Action {
    Ok("It works!")
  }

}

简单结果

响应结果通过 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"))
  )
}

不过play提供了快捷方法 Ok() :

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

其它简便方法:

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

重定向

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

TODO 页面

暂未实现的页面

def index(name:String) = TODO

第二章 HTTP 路由

内置HTTP路由

路由用于将HTTP请求导向 Action

HTTP请求被MVC视为事件处理,事件包括两部分信息:

  • 请求路径,包括查询字符串
  • 请求方法

路由表定义在 conf/routes 文件中,它也会被编译,如果路由规则书写错误,Play将抛出异常。

依赖注入

Play支持生成两种类型的路由:

  • 依赖注入路由
  • 静态路由

默认是依赖注入路由,如果需要使用静态路由,需要在 build.sbt 中添加如下配置:

routesGenerator := StaticRoutesGenerator

路由文件格式

conf/routes 中定义了应用所有的路由规则,每个路由包括了请求方法和URL规则,它们与 Action 的调用相关联。

例如,如下路由规则:

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

每个路由规则都以请求方法开始,然后是URL规则,最后是 Action 调用的定义。

也可以在文件中添加注释,以 # 开头:

# Display a client.
GET   /clients/:id          controllers.Clients.show(id: Long)

也可以使用别的路由表文件,使用 ->

->      /api                        api.MyRouter

HTTP方法

Play支持的HTTP方法包括 GET , POST , PUT , DELETE , HEAD

URL规则

URL规则定义了路由的请求路径,请求路径可以包含动态部分。

静态路径

例如,定义 GET /clients/all 规则:

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

动态路径

如果你需要从路由中获取 clientid ,可以这样配置:

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

一个路由规则可以有多个动态部分。

默认的路由匹配规则实际由正则表达式 [^/]+ 表示。

如果需要匹配包含 / 的URL,可以使用 *id 的语法,它会采用 .* 的正则表达式:

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

例如,对于 GET /files/images/logo.pngname 将匹配 images/logo.png

Play还支持自定义URL规则,使用 $id<regex> 语法:

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

调用Action生成器方法

路由定义的最后一部分就是调用 Action 生成方法,这部分必须定义一个合法的方法,该方法返回一个 Action 类型的值。

如果方法没有定义任何参数:

如果方法定义了参数,则参数值将从请求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)

下面是对应的方法:

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

参数类型

如果参数类型为 String ,可以不注明参数类型,如果需要将参数转换为特定的 Scala 类型,需要明确指定参数类型:

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

指定参数值

有时候需要指定参数的值:

# 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])

路由权重

优先匹配首先定义的规则

反向路由

也可以通过调用的方法反向生成URL,对于路由规则中的 controller ,play会在 routes 目录中生成一个反向控制器,返回 play.api.mvc.Call

play.api.mvc.Call 定义了一个HTTP调用,它提供了请求方法和URI。

例如:

package controllers

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

class Application extends Controller {

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

}

映射到路由表:

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

可以反向获取 hello 方法的URL:

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

默认路由

Play提供了一些默认的路由:

# Redirects to https://www.playframework.com/ with 303 See Other
GET   /about      controllers.Default.redirect(to = "https://www.playframework.com/")

# Responds with 404 Not Found
GET   /orders     controllers.Default.notFound

# Responds with 500 Internal Server Error
GET   /clients    controllers.Default.error

# Responds with 501 Not Implemented
GET   /posts      controllers.Default.todo

第三章 处理响应结果

修改默认的Content-Type

Play可以自动根据响应内容推断返回的数据类型。例如:

val textResult = Ok("Hello World!")

会自动设置 Content-Type为text/plain

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

会设置 Content-Type为application/xml

这些是通过 play.api.http.ContentType 来实现的。

不过我们也可以手动设置返回类型。

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

或者:

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

设置HTTP headers

我们也可以设置或者更新HTTP头部信息。

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

设置响应数据编码格式

Play默认使用UTF-8编码,不过也可以人工指定。只需要声明一个隐式参数转换就可以。

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

}

第四章 Session和Flash

如果需要在多个HTTP请求中传递数据,可以使用 Session 或者 Flash 。它们的区别在于:

  • Session 中的数据会在整个会话过程中一直保存
  • Flash 中数据只会保存到下一次请求

由于Play中 sessionflash 的数据都只是保存在接下来的请求中,而不是服务器中,所以对保存的数据大小有限制,最大为4kb。默认的 cookie 名为 PLAY_SESSION,可以编辑配置的 session.cookieName 进行修改。

在session中保存数据

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

上面的代码会替换整个 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")
  }

删除整个会话

Ok("Bye").withNewSession

第五章 body parser

一个HTTP请求其实就是一个请求头跟着一个请求体。

请求头使用 RequestHeader 处理,请求体使用 BodyParser 处理。

由于Play是一个异步框架,所以传统的 InputStream 并不能用来读取请求体数据。Play使用异步流库 Akka Streams 来读取数据。

内置解析器

大多数web应用都不用使用自定义解析器,一般我们不需要指明使用哪个解析器,Play会根据请求体重的 Content-Type 类型来推断要使用的解析器。

第六章 action组合

自定义action

创建 action 的方法定义在特质 ActionBuilder 中,我们创建的 action 实际上是 ActionBuilder 特质的实例。

如果要实现自定义 action ,只需要继承特质 ActionBuilder ,并实现 invokeBlock 方法,下面实现一个自定义 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)
  }
}

现在可以使用刚才定义的 LogginAction 了:

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

组合action

在很多应用中,我们可能会定义很多类型的 Action 创建器,有的需要认证,有的可以记录日志。

那么可不可以重用这些代码呢?

第七章 内容协商

内容协商

HTTP通过 Accept 头部俩指明请求体的格式:

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() 都是提取器。

请求提取器

Play 的 Accepts 支持以下 MIME 类型:

  • Xml
  • Html
  • Json
  • JavaScript

也可以自定义 MIME 类型,只需使用 ``play.api.mvc.Accepting``类即可。

第八章 异常处理

HTTP程序可以返回两种错误:

  • 客户端错误
  • 服务器端错误

Play在大多数情况下都可以自动检测到客户端的错误,例如请求头错误,不支持的数据类型等。

Play也能处理服务器端错误,只要你在 Action 中抛出异常,Play就会返回一个异常的页面给客户端。

Play处理错误的接口是 HttpErrorHanlder ,它定义了两个方法:

  • onClientError
  • onServerError

自定义错误处理器

自定义错误处理可以这样实现:在项目根目录中创建 ErrorHandler 类,这个类需要继承 HttpErrorHandler 。例如:

import play.api.http.HttpErrorHandler
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent._
import javax.inject.Singleton;

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

也可以将这个类放在其他包中,然后在 application.conf 中指明位置:

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

继承默认的Error Handler

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._

@Singleton
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.")
    )
  }
}

第九章 异步HTTP编程

处理异步结果

Play原生支持异步HTTP编程,它以异步无阻塞的方式处理客户端请求。

Play中 controller 默认是异步,在Play运行过程中, action 的代码必须执行速度比较快,否则会阻塞,那么如何处理需要计算很长时间的任务呢?

答案是使用 future 作为返回结果。

Future[Result] 最终将赋值为 Result 类型,通过将 Future[Result] 替换为 Result ,我们就可以快速生成响应而不用等待。

客户端会一直处于阻塞等待响应,但是服务端不会。

为了返回 Future[Result] ,需要先创建一个 Future ,以便在以后给 Result 赋值。

import play.api.libs.concurrent.Execution.Implicits.defaultContext

val futurePIValue: Future[Double] = computePIAsynchronously()
val futureResult: Future[Result] = futurePIValue.map { pi =>
  Ok("PI value computed: " + pi)
}

返回异步结果

之前我们一直使用 apply 来生成 action ,为了返回异步结果,需要使用 Action.async 方法:

import play.api.libs.concurrent.Execution.Implicits.defaultContext

def index = Action.async {
  val futureInt = scala.concurrent.Future { intensiveComputation() }
  futureInt.map(i => Ok("Got result: " + i))
}

Actions 默认也是异步的。

处理超时

为了避免客户端一直处于等待中,可以使用 promise timeout 来处理:

import play.api.libs.concurrent.Execution.Implicits.defaultContext
import scala.concurrent.duration._

def index = Action.async {
  val futureInt = scala.concurrent.Future { intensiveComputation() }
  val timeoutFuture = play.api.libs.concurrent.Promise.timeout("Oops", 1.second)
  Future.firstCompletedOf(Seq(futureInt, timeoutFuture)).map {
    case i: Int => Ok("Got result: " + i)
    case t: String => InternalServerError(t)
  }
}

流式响应

在现实中,我们可能需要发送大量的数据,这时可以采用流式响应:

分发文件

在Play中分发文件也很方便:

def index = Action {
 Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}

Play会自动计算 Conten-TypeContent-Disposition,不过也可以自定义:

def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    fileName = _ => "termsOfService.pdf"
 )
}

如果你想将文件直接显示在浏览器中,可以这样设置:

def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    inline = true
  )
}

块响应

如果后台数据是动态生成的,这时没法计算数据大小,只能分块发送。

def index = Action {
  val CHUNK_SIZE = 100
  val data = getDataStream
  val dataContent: Source[ByteString, _] = StreamConverters.fromInputStream(data, CHUNK_SIZE)

  Ok.chunked(dataContent)
}

当然,我们也可以使用任意 Source 的块数据:

def index = Action {
  val source = Source.apply(List("kiki", "foo", "bar"))
  Ok.chunked(source)
}

第十章 模板引擎

Play框架的模板使用的是Twirl,它使基于scala实现的,它具有以下优点:

  • 稳固、容易表达、流畅
  • 容易学习
  • 使用scala语法
  • 可以在任意编辑器中进行编辑

所有模板文件都必须编译之后才能运行,所以可以在浏览器中查看错误信息。

模板语法

概览

Play Scala模板是一个包含scala代码的文本文件,它可以生成各种文本格式的文件,例如HTML、XML、CSV等。这个模板框架让开发者很容易的进行前后端的开发。

每个模板都会根据规范被编译成标准的的Scala函数,如果模板文件为 views/Application/index.scala.html ,则编译成类 views.html.Application.index , 这个类具有 apply() 方法。

例如,下面模板:

@(customer: Customer, orders: List[Order])

<h1>Welcome @customer.name!</h1>

<ul>
@for(order <- orders) {
  <li>@order.title</li>
}
</ul>

定义上述模板之后,从任意其他Scala代码中,我们可以调用下面的方法:

val content = views.html.Application.index(c, o)

魔术字符@

Scala模板有且只有一个魔法字符 @ ,每当遇到这个字符的时候,就表明scala语句的开始,但是我们不需要像要其它模板语言一样,闭合scala代码段,Scala会自动判断代码的结束。

Hello @customer.name!
       ^^^^^^^^^^^^^
       Dynamic code

如果想插入一条包含多个参数的scala代码,可以使用括号显示声明。

还可以使用花括号插入多条scala声明语句:

Hello @{val name = customer.firstName + customer.lastName; name}!
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                          Dynamic Code

由于 @ 是特殊字符,所以有时候需要进行转义,使用 @@ 进行转义

My email is bob@@example.com

模板参数

一个模板其实就像一个scala函数,所以它需要参数,这些参数必须在模板的开始处进行声明。

@(customer: Customer, orders: List[Order])

还可以给参数设置默认值:

@(title: String = "Home")

甚至可以传递参数组合:

@(title: String)(body: Html)

循环

可以使用scala的 for 循环语句进行循环操作:

<ul>
@for(p <- products) {
  <li>@p.name ($@p.price)</li>
}
</ul>

注意,必须确保 { 必须与 for 位于同一行。

判断

模板中的判断语句与scala一样。

@if(items.isEmpty) {
  <h1>Nothing to display</h1>
} else {
  <h1>@items.size items!</h1>
}

创建可复用代码块

相当于创建一个宏命令:

@display(product: Product) = {
  @product.name ($@product.price)
}

<ul>
@for(product <- products) {
  @display(product)
}
</ul>

也可以定义完全由scala组成的复用代码:

@title(text: String) = @{
  text.split(' ').map(_.capitalize).mkString(" ")
}

<h1>@title("hello world")</h1>

惯例情况下,如果可复用代码块中名字前带有 implicit ,它就需要标注为 implicit

@implicitFieldConstructor = @{ MyFieldConstructor() }

定义可复用的变量

使用 defining 定义可复用的变量。

@defining(user.firstName + " " + user.lastName) { fullName =>
  <div>Hello @fullName</div>
}

导入声明

可以在模板的开头导入任何你想导入的包。

@(customer: Customer, orders: List[Order])
@import utils._

如果想使用绝对路径的话,在导入语句前面使用 root

@import _root_.company.product.core._

如果需要在所有模板中都导入同一个包,可以在 build.sbt 中进行声明。

TwirlKeys.templateImports += "org.abc.backend._"

注释

使用 @**@ 进行注释:

@*********************
  * This is a comment *
*********************@

还可以在模板文件的开头注释,以保存到Scala的API文档中。

@*************************************
* Home page.                        *
*                                   *
* @param msg The message to display *
*************************************@
@(msg: String)

<h1>@msg</h1>

转义

默认情况下,动态内容根据模板类型解析成相应的格式文件,如果只想输出原始的内容片段,将它包含在模板内容类型中即可:

<p>
  @Html(article.content)
</p>

模板常见用法

Play框架中,模板其实就是函数,它可以被暴露在任意位置。下面是模板的常见用法。

Layout

首先创建模板 views/main.scala.html ,它将作为其它模板的基础模板。

从上可知,该模板接收两个参数: titleHTML 内容块。定义好基础模板之后,就可以从其它模板中引用这个模板了。创建模板 views/Application/index.scala.html

@main(title = "Home") {

  <h1>Home page</h1>

}

也许我们还需要定义一个侧边栏。

@(title: String)(sidebar: Html)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
    <section class="sidebar">@sidebar</section>
    <section class="content">@content</section>
  </body>
</html>

这种情况下,继承模板这么写:

也可以分开来写:

@sidebar = {
  <h1>Sidebar</h1>
}

@main("Home")(sidebar) {
  <h1>Home page</h1>

}

第十一章 表单

待续

第十二章 JSON处理

JSON基础

待续

JSON与HTTP

待续

JSON 组合器

JSON 自动映射

JSON转换器

第十三章 XML处理

待续

第十四章 文件上传

使用表单上传文件

标准的文件上传方式是通过表单形式来上传,HTML表单如下:

后台上传代码如下:

public Result upload() {
    MultipartFormData<File> body = request().body().asMultipartFormData();
    FilePart<File> picture = body.getFile("picture");
    if (picture != null) {
        String fileName = picture.getFilename();
        String contentType = picture.getContentType();
        File file = picture.getFile();
        return ok("File uploaded");
    } else {
        flash("error", "Missing file");
        return badRequest();
    }
}

直接文件上传

另一种方式是通过JSON方式上传:

public Result upload() {
    File file = request().body().asRaw().asFile();
    return ok("File uploaded");
}

第十五章 数据库操作

待续

第十六章 缓存

Play 默认使用 EHCache 实现缓存API。你也可以自定义插件实现。

添加依赖

build.sbt 中添加依赖:

libraryDependencies ++= Seq(
  cache
)

访问缓存API

缓存API由 CacheApi 对象提供,可以以依赖的形式插入到对象中。

import play.api.cache._
import play.api.mvc._
import javax.inject.Inject

class Application @Inject() (cache: CacheApi) extends Controller {

}

保存数据

cache.set("item.key", connectedUser)

还可以设置有效时间: .. code-block:: scala

import scala.concurrent.duration._

cache.set(“item.key”, connectedUser, 5.minutes)

获取数据

当没有找到数据的时候,还可以提供一个可调用对象作为附加参数。

val user: User = cache.getOrElse[User]("item.key") {
  User.findById(connectedUser)
}

删除数据

cache.remove("item.key");

访问不同的缓存

默认情况下缓存保存在 play 中,如果需要保存到不同的缓存,需要在 application.conf 中进行配置。

play.cache.bindCaches = ["db-cache", "user-cache", "session-cache"]

接下来就可以使用 NamedCache 来访问这些缓存了:

import play.api.cache._
import play.api.mvc._
import javax.inject.Inject

class Application @Inject()(
    @NamedCache("session-cache") sessionCache: CacheApi
) extends Controller {

}

缓存HTTP响应

Play的HTTP响应可以被缓存然后再使用,使用 Cached 类创建缓存:

import play.api.cache.Cached
import javax.inject.Inject

class Application @Inject() (cached: Cached) extends Controller {

}

使用固定的键来缓存响应结果:

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

如果结果是变化的,可以用不同的 key 来缓存:

```

缓存控制

控制缓存的结果也非常简单,下面的例子值只缓存响应码为200 的结果。

def get(index: Int) = cached.status(_ => "/resource/"+ index, 200) {
  Action {
    if (index > 0) {
      Ok(Json.obj("id" -> index))
   } else {
     NotFound
    }
  }
}

或者只缓存404几分钟:

def get(index: Int) = {
  val caching = cached
    .status(_ => "/resource/"+ index, 200)
   .includeStatus(404, 600)

 caching {
    Action {
      if (index % 2 == 1) {
       Ok(Json.obj("id" -> index))
     } else {
       NotFound
     }
    }
 }
}

第十七章 web服务

有时候我们需要调用其它站点的HTTP服务,Play通过WS库来支持这些异步调用。

调用WS API包括两个部分,发起请求,处理响应。

发送请求

使用WS之前,需要在build.sbt中添加依赖:

libraryDependencies ++= Seq(
  ws
)

接下来就可以在组件中通过声明WSClient的注入来调用WS服务。

import javax.inject.Inject
import scala.concurrent.Future
import scala.concurrent.duration._

import play.api.mvc._
import play.api.libs.ws._
import play.api.http.HttpEntity

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.stream.scaladsl._
import akka.util.ByteString

import scala.concurrent.ExecutionContext

class Application @Inject() (ws: WSClient) extends Controller {

}

使用 ws.url() 发送请求:

val request: WSRequest = ws.url(url)

返回一个 WSRequest 对象,可以给它指定各种HTTP选项,比如头部信息,可以通过链式调用来构建复杂的请求:

val complexRequest: WSRequest =
request.withHeaders("Accept" -> "application/json")
  .withRequestTimeout(10000.millis)
  .withQueryString("search" -> "play")

最后调用你想使用的HTTP方法结束请求:

val futureResponse: Future[WSResponse] = complexRequest.get()

请求结果以 Future[WSResponse] 形式返回, WSResponse 包含了服务端返回的数据。

带认证的请求

如果需要HTTP认证,可以在请求中指明认证方式以及相关参数,WS支持的认证模式包括以下几种:

  • BASIC
  • DIGEST
  • KERBEROS
  • NTLM
  • SPNEGO
ws.url(url).withAuth(user, password, WSAuthScheme.BASIC).get()

带重定向的请求

如果请求结果导致了320或者301重定向,可以自动重定向,而不是再发送一次请求:

ws.url(url).withFollowRedirects(true).get()

带头部信息的请求

HTTP头部信息可以通过一系列的键值对元组来指明:

ws.url(url).withHeaders("headerKey" -> "headerValue").get()

例如,通过设置 Content-Type 来指定请求中发送的数据类型:

ws.url(url).withHeaders("Content-Type" -> "application/xml").post(xmlString)

带虚拟主机的请求

可以在请求中指明虚拟主机:

ws.url(url).withVirtualHost("192.168.1.1").get()

设置超时时间

如果需要指定请求的超时时间,可以使用 withRequestTimeout``来设置, 如果要一直等待下去,可以使用 ``Duration.Inf 作为参数。

ws.url(url).withRequestTimeout(5000.millis).get()

提交表单数据

将表单数据以 Map[String, Seq[String]]] 的形式作为参数传递给 post 方法:

ws.url(url).post(Map("key" -> Seq("value")))

提交multipart/form数据

如果是提交 multipart-form-encoded 数据,则需要将 Source[play.api.mvc.MultipartFormData.Part[Source[ByteString, Any]], Any]``类型的数据作为参数传递给 ``post 方法:

ws.url(url).post(Source.single(DataPart("key", "value")))

如果是上传文件,则需要将 play.api.mvc.MultipartFormData.FilePart[Source[ByteString, Any]] 类型的数据传递给 post 方法:

ws.url(url).post(Source(FilePart("hello", "hello.txt", Option("text/plain"), FileIO.fromFile(tmpFile)) :: DataPart("key", "value") :: List()))

提交JSON数据

使用JSON库即可:

提交XML数据

提交XML数据最简单的方式就是使用XML字面量,XML字面量虽然方便,但是并不快,为了方便起见,可以使用XML视图模板或者JAXB库。

val data = <person>
  <name>Steve</name>
  <age>23</age>
</person>
val futureResponse: Future[WSResponse] = ws.url(url).post(data)

流数据

WS还支持流数据,用于上传大型文件,如果数据库支持Reactive Streams,则可以使用流数据:

val wsResponse: Future[WSResponse] = ws.url(url)
  .withBody(StreamedBody(largeImageFromDB)).execute("PUT")

上面l largeImageFormDB 的数据类型为 Source[ByteString, _]

过滤请求

还可以给WSRequest添加一个请求过滤器,请求过滤器通过继承特质 `` play.api.libs.ws.WSRequestFilter`` 来实现,然后使用 request.withRequestFilter(filter) 将它添加请求中.

WS提供了一个过滤器的实现,位于 play.api.libs.ws.ahc.AhcCurlRequestLogger ,它用于将请求的信息以SLF4J日志形式进行记录。

ws.url(s"http://localhost:$testServerPort")
.withRequestFilter(AhcCurlRequestLogger())
.withBody(Map("param1" -> Seq("value1")))
.put(Map("key" -> Seq("value")))

将输入以下日志:

curl \
--verbose \
--request PUT \
--header 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
--data 'key=value' \
'http://localhost:19001/

处理响应结果

响应结果包裹在 Future 中。

Future 中的计算任务完成是,需要指定一个隐式参数–执行上下文, ``Future``的回调将在该上线文的线程池中执行。我们可以通过依赖注入指定执行上下文

class PersonService @Inject()(implicit context: ExecutionContext) {
  // ...
}

如果不适用依赖注入,也可以使用默认的执行上下文:

implicit val context = play.api.libs.concurrent.Execution.Implicits.defaultContext

下面的例子使用都使用如下定义的样式类进行序列化和反序列化:

case class Person(name: String, age: Int)

将响应解析为JSON

调用 response.json 将响应数据解析为 JSON 对象。

val futureResult: Future[String] = ws.url(url).get().map {
 response =>
    (response.json \ "person" \ "name").as[String]
}

JSON库可以将隐式参数 Reads[T] 映射成相应的类:

import play.api.libs.json._

implicit val personReads = Json.reads[Person]

val futureResult: Future[JsResult[Person]] = ws.url(url).get().map {
  response => (response.json \ "person").validate[Person]
}

将响应解析为XML

使用 response.xml 将数据解析为XML:

val futureResult: Future[scala.xml.NodeSeq] = ws.url(url).get().map {
response =>
  response.xml \ "message"
}

处理大响应数据

当调用 get()post() 或者 execute() 方法的时候,响应数据会加载到内存中,如果数据比较大会容易导致内存错误。

WS 库允许我们逐步的读取响应数据,通过使用Akka的 Sink

WSRequest``的 ``stream() 方法返回 Future[StreamedResponse], 而 StreamResponse 是一个保存响应头部和响应体的容器。

// Make the request
val futureResponse: Future[StreamedResponse] =
  ws.url(url).withMethod("GET").stream()

val bytesReturned: Future[Long] = futureResponse.flatMap {
  res =>
   // Count the number of bytes returned
   res.body.runWith(Sink.fold[Long, ByteString](0L){ (total, bytes) =>
      total + bytes.length
    })
}

也可以将响应数据保存在文件中:

// Make the request
val futureResponse: Future[StreamedResponse] =
  ws.url(url).withMethod("GET").stream()

val downloadedFile: Future[File] = futureResponse.flatMap {
  res =>
   val outputStream = new FileOutputStream(file)

   // The sink that writes to the output stream
   val sink = Sink.foreach[ByteString] { bytes =>
      outputStream.write(bytes.toArray)
    }

    // materialize and run the stream
   res.body.runWith(sink).andThen {
      case result =>
       // Close the output stream whether there was an error or not
       outputStream.close()
       // Get the result or rethrow the error
       result.get
   }.map(_ => file)
}

还有就是将响应数据返回给Action:

def downloadFile = Action.async {

// Make the request
ws.url(url).withMethod("GET").stream().map {
  case StreamedResponse(response, body) =>

    // Check that the response was successful
    if (response.status == 200) {

      // Get the content type
      val contentType = response.headers.get("Content-Type").flatMap(_.headOption)
        .getOrElse("application/octet-stream")

      // If there's a content length, send that, otherwise return the body chunked
      response.headers.get("Content-Length") match {
        case Some(Seq(length)) =>
          Ok.sendEntity(HttpEntity.Streamed(body, Some(length.toLong), Some(contentType)))
        case _ =>
          Ok.chunked(body).as(contentType)
      }
    } else {
      BadGateway
    }
 }
}

从上面我们可以注意到可以通过 withMethod 指定请求方法:

val futureResponse: Future[StreamedResponse] =
  ws.url(url).withMethod("PUT").withBody("some body").stream()

第十八章 Akka

Akka用于构建高并发可扩展应用,它的容错性很高。

actor系统

Akka工作在 actor 系统之上, actor 系统用于管理资源。

Play应用定义了一个特殊的 actor 系统,这个系统的生命周期与 play 应用的生命周期保持一致。

定义actors

使用Akka之前,需要先创建一个 actor

import akka.actor._

object HelloActor {
 def props = Props[HelloActor]

 case class SayHello(name: String)
}

class HelloActor extends Actor {
  import HelloActor._

  def receive = {
    case SayHello(name: String) =>
      sender() ! "Hello, " + name
  }
}

上面的 actor 遵守了Akka规范:

  • 发送或者接受的消息以及协议, 都定义在伴生对象中
  • 定义了props方法

创建和使用actors

创建actor,需要使用 ActorSystem ,通过依赖注入实现:

import play.api.mvc._
import akka.actor._
import javax.inject._

import actors.HelloActor

@Singleton
class Application @Inject() (system: ActorSystem) extends Controller {

  val helloActor = system.actorOf(HelloActor.props, "hello-actor")

 //...
}

actorOf 方法用来创建 actor ,注意这里 Controller 声明为单例类,这是因为 actor 与类相关联,不能创建两个名字相同的 actor

发送消息

actor 最基本的用法就是发送消息,当发送消息给 actor 的时候,并没有返回。

但是HTTP是需要返回的,这时可以返回 Future 结果类型。

import play.api.libs.concurrent.Execution.Implicits.defaultContext
import scala.concurrent.duration._
import akka.pattern.ask
implicit val timeout: Timeout = 5.seconds

def sayHello(name: String) = Action.async {
  (helloActor ? SayHello(name)).mapTo[String].map { message =>
    Ok(message)
  }
}

需要注意的是:

  • 使用 操作符表示等待返回
  • 返回结果是 Future[Result] 类型
  • 需要声明隐式参数 timeout

依赖注入 actors

第十九章 国际化

待续

第二十章 依赖注入

Play框架支持运行时依赖注入和编译时依赖注入。

运行时依赖注入是指依赖路径在程序运行时才会创建,如果没有找到相应依赖,运行时并不会报错。

目的

依赖注入实现以下目的:

  • 实现松耦合绑定对象
  • 避免了全局静态状态

运行时依赖注入

如果你有一个组件,比如 controller ,依赖于其他组件,这种情况可以是使用 @Inject 注解来声明。

@Inject 注解可以用在字段或者构造器中,推荐使用在构造器中。

import javax.inject._
import play.api.libs.ws._

class MyComponent @Inject() (ws: WSClient) {
  // ...
}

注意 @Inject 注解必须位于类名之后,构造器参数之前,并且必须有 ()

除此之外,``Guice``还支持其他几种依赖注入方式,但是构造器注入是最简洁的方式。

依赖注入控制器

有两种方式使用依赖控制器。

  • 注入路由生成器
  • 静态路由生成器

默认情况下,Play会将路由对象的 controller 声明为依赖,如果需要显示声明这个依赖,可以在 build.sbt 中进行配置:

routesGenerator := InjectedRoutesGenerator

通过在 build.sbt 中进行如下配置,声明静态路由生成器:

routesGenerator := StaticRoutesGenerator

推荐使用注入路由生成器。

组件声明周期

依赖注入管理被注入组件的生命周期,当需要这些组件的时候,才会创建,然后注入到其它组件中。

组件生命周期工作方式如下:

  • 当需要组件时进行创建,如果被使用多次,默认将会创建多个实例,如果你需要的是一个单例,需要将组件标记为 singleton
  • 组件实例都是懒创建的,只有在需要的时候才会创建。
  • 组件实例都会自动清除,它组件不再被引用时,将自动清除。

Singletons

有时候我们只需要创建一个对象实例,比如数据库连接,这时可以使用 @Singleton 注解实现:

import javax.inject._

@Singleton
class CurrentSharePrice {
  @volatile private var price = 0

  def set(p: Int) = price = p
  def get = price
}

清除

有些组件在Play关闭之后需要做一些清理工作,这可以通过 ApplicationLifecycle 组件实现:

import scala.concurrent.Future
import javax.inject._
import play.api.inject.ApplicationLifecycle

@Singleton
class MessageQueueConnection @Inject() (lifecycle: ApplicationLifecycle) {
 val connection = connectToMessageQueue()
 lifecycle.addStopHook { () =>
   Future.successful(connection.stop())
 }

 //...
}

注意,必须确保注册了stop钩子的类为单例类,否则容易发生内存泄露。

处理循环依赖

循环依赖发生在如下情况:

import javax.inject.Inject

class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Foo)

可以使用 Provider 解决这个问题:

import javax.inject.{ Inject, Provider }

class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Provider[Foo])

第二十一章 项目配置

Play配置文件基于 Typesafe.config 配置库, Typesafe.Config 库,纯Java写成、零外部依赖、代码精简、功能灵活、API友好。支持 Java properties、JSON、JSON 超集格式 HOCON 以及环境变量。它也是 Akka 的配置管理库。

配置信息必须定义在 conf/application.conf ,它采用 HOCON 格式。

指定配置文件

运行程序时,默认从 application.conf 中加载配置,不过也可以指定配置文件。

  • config.resource 指定配置文件名以及后缀。
  • config.file 指定配置文件所在路径。

通过这两种方式指定配置文件会替换掉默认的配置文件。

还可以在配置文件中使用 include application 引用其他配置文件。

与Akka一起使用

Akka与Play使用同一个配置文件 application.conf

与run命令一起使用

当通过 run 命令加载配置文件时,要注意如下事项:

格外的devSettings

可以在 build.sbt 中配置格外信息,当我们部署到生产环境中时,不会使用这些配置信息。

PlayKeys.devSettings := Seq("play.server.http.port" -> "8080")

application.conf中的配置服务器信息

run 模式下,Play服务器首先运行,然后再编译源代码。这也就意味着HTTP服务器刚开始运行时无法读取 application.conf 中的配置信息。如果你需要覆写HTTP服务器的信息,就不能使用 application.conf 进行配置,比如服务器端口,这个时候可以直接在命令行中配置:

> run -Dhttp.port=1234

HOCON语法

HOCON 的语法格式与 JSON 类似。

与JSON保持一致的部分

  • 文件必须以UTF-8编码
  • 引号括起来的字符串与JSON字符串一样
  • 取值类型可以为:字符串,数值,对象,数组,布尔值,null
  • 不支持 NaN

注释

使用 # 或者 // 进行注释

省略根括号

JSON格式中根元素必须是个数组或对象。空文档是非法的格式。

而在 HOCON 中,如果文档不是以[或者{开始的话,解析的时候默认会给它加上{}。

如果一个 HOCON 文档省略了{但是末尾还是有}的话,文档还是合法的。

键值对

在JSON中配置采用键值对的形式 key:value ,而在 HOCON 中,可以使用 = 替换

如果 key 的后面是 { ,则 = 或者 都可以省略。所以 "foo" {} 等价于 "foo":{}

逗号

数组中的变量,以及对象中的字段,它们中间不一定需要,分开,也可以使用 \n 分隔。 数组中的最后一个元素或者字典中的最后一个字段后面可以跟一个逗号。这个逗号会被忽略掉。 所以:

  • [1,2,3] 等价于 [1\n2\n3]
  • [1,2,3] 等价于 [1,2,3,]

重复的键

JSON格式中并没有说明如何处理重复的键。

HOCON 中,如果出现重复的键,后面的键值会覆盖掉前面的键值,除非键值的类型为字典,如果为字典,则两个键值进行合并。

路径形式的key

如果可以以路径的形式出现,相当于针对每个路径元素创建对象。 例如:

foo.bar:42

等价于

foo {bar:42}

导入配置文件

使用 include "file" 的形式导入配置文件。

第二十二章 测试

待续

第二十三章 日志

Play框架提供了一个日志接口。这个日志接口包括如下几个部分:

  • Logger
  • Log levels
  • Appenders

Logger

在应用中创建Logger的实例来发送日志信息。每个 Logger 实例都有一个名字,以便区分。

Logger以名字中的点号区分继承关系。例如,以 'com.foo' 命名的 Logger'com.foo.bar.Baz' 的父 Logger 。所有的 logger``都继承自一个根 ``logger ,通过 logger 继承可以配置一套的 logger

Play应用提供了一个默认的名字为 applicationLogger

日志级别

日志级别用于区分不同类型的日志信息。

Play中日志级别如下:

  • OFF 禁用日志信息
  • ERROR 运行错误
  • WARN 警告信息
  • INFO 日志信息
  • DEBUG 调试信息
  • TRACE 详细信息

输出源

Appenders 用于定义日志的输出源,日志API支持输出日志信息到不同的地方,通过配置可以实现输出日志到命令行,数据库等等。

使用日志

首先导入 Logger 类及其伴生对象

import play.api.Logger

默认Logger

Logger 对象是默认的 Logger ,它的名字是 application

自定义Logger

通过 Loggerapply 方法可以创建一个 Logger

val accessLogger: Logger = Logger("access")

不过更普遍的方法针对每个类使用该类的类名作为 Logger 的名字:

val logger: Logger = Logger(this.getClass())

日志配置

Play框架使用 SLF4J 进行日志记录,背后使用 Logback 作为日支引擎。