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")
}
第二章 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规则定义了路由的请求路径,请求路径可以包含动态部分。
动态路径¶
如果你需要从路由中获取 client
的 id
,可以这样配置:
GET /clients/:id controllers.Clients.show(id: Long)
一个路由规则可以有多个动态部分。
默认的路由匹配规则实际由正则表达式 [^/]+
表示。
如果需要匹配包含 /
的URL,可以使用 *id
的语法,它会采用 .*
的正则表达式:
GET /files/*name controllers.Application.download(name)
例如,对于 GET /files/images/logo.png
,name
将匹配 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")
设置或删除Cookie¶
设置Cookie
val result = Ok("Hello world").withCookies(
Cookie("theme", "blue"))
删除Cookie:
val result2 = result.discardingCookies(DiscardingCookie("theme"))
设置并删除Cookie:
val result3 = result.withCookies(Cookie("theme", "blue")).discardingCookies(DiscardingCookie("skin"))
设置响应数据编码格式¶
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中 session
或 flash
的数据都只是保存在接下来的请求中,而不是服务器中,所以对保存的数据大小有限制,最大为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")
}
第七章 内容协商¶
内容协商¶
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()
都是提取器。
第八章 异常处理¶
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-Type
和 Content-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>
模板常见用法¶
Play框架中,模板其实就是函数,它可以被暴露在任意位置。下面是模板的常见用法。
Layout¶
首先创建模板 views/main.scala.html
,它将作为其它模板的基础模板。
从上可知,该模板接收两个参数: title
和 HTML
内容块。定义好基础模板之后,就可以从其它模板中引用这个模板了。创建模板 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>
}
第十一章 表单¶
待续
第十三章 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。你也可以自定义插件实现。
访问缓存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()
带头部信息的请求¶
HTTP头部信息可以通过一系列的键值对元组来指明:
ws.url(url).withHeaders("headerKey" -> "headerValue").get()
例如,通过设置 Content-Type
来指定请求中发送的数据类型:
ws.url(url).withHeaders("Content-Type" -> "application/xml").post(xmlString)
设置超时时间¶
如果需要指定请求的超时时间,可以使用 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,]
导入配置文件¶
使用 include "file"
的形式导入配置文件。
第二十二章 测试¶
待续
第二十三章 日志¶
Play框架提供了一个日志接口。这个日志接口包括如下几个部分:
- Logger
- Log levels
- Appenders
Logger¶
在应用中创建Logger的实例来发送日志信息。每个 Logger
实例都有一个名字,以便区分。
Logger以名字中的点号区分继承关系。例如,以 'com.foo'
命名的 Logger
是 'com.foo.bar.Baz'
的父 Logger
。所有的 logger``都继承自一个根 ``logger
,通过 logger
继承可以配置一套的 logger
。
Play应用提供了一个默认的名字为 application
的 Logger
。
日志级别¶
日志级别用于区分不同类型的日志信息。
Play中日志级别如下:
- OFF 禁用日志信息
- ERROR 运行错误
- WARN 警告信息
- INFO 日志信息
- DEBUG 调试信息
- TRACE 详细信息
输出源¶
Appenders
用于定义日志的输出源,日志API支持输出日志信息到不同的地方,通过配置可以实现输出日志到命令行,数据库等等。
默认Logger¶
Logger
对象是默认的 Logger
,它的名字是 application
。
自定义Logger¶
通过 Logger
的 apply
方法可以创建一个 Logger
val accessLogger: Logger = Logger("access")
不过更普遍的方法针对每个类使用该类的类名作为 Logger
的名字:
val logger: Logger = Logger(this.getClass())
日志配置¶
Play框架使用 SLF4J
进行日志记录,背后使用 Logback
作为日支引擎。