该API直接位于 play 顶级包中(而play.mvc是为Java开发者提供的)。对于Scala开发者,查阅play.api.mvc。
Actions, Controllers and Results
什么是Action?
大多数Play应用程序接受的请求由一个Action处理。
一个play.api.mvc.Action基本上是 一个 (play.api.mvc.Request => play.api.mvc.Result)函数,它处理请求并生成响应发给客户端。
val echo = Action { request => Ok("Got request [" + request + "]") }
action返回一个 play.api.mvc.Result对象,使用 HTTP response 对象返回给客户端。例如: Ok 返回一个200响应,包含text/plain 响应体。
创建Action
最简单的Action仅需要定义一个参数,一个表达式块,返回一个Result值
Action { Ok("Hello world") }
这是创建Action最简单的方式,但我们无法获取request对象。通常Action中都需要访问request对象。
看看第二个Action,包含了参数Request => Result :
Action { request => Ok("Got request [" + request + "]") }
标记 request 参数为 隐式 通常都很有用,可供其它API隐式的使用:
Action { implicit request => Ok("Got request [" + request + "]") }
最后一种创建方式,包含了一个特别的可选 BodyParser 参数:
Action(parse.json) { implicit request => Ok("Got request [" + request + "]") }
Body Parser稍后会做讲解。现在,你只需要了解Any content body parser的使用方式。
控制器是actions的生成器
控制器不过是产生Action的某个单例对象。
定义Action生成器的最简单方法是提供一个无参,返回值为Action的方法。
package controllers import play.api.mvc._ object Application extends Controller { def index = Action { Ok("It works!") } }
当然,该方法可以包含参数,这些参数可以被Action闭包访问:
def hello(name: String) = Action { Ok("Hello " + name) }
简单Results
目前,你可能只对一种results感兴趣:HTTP result,包含状态字,一系列HTTP Head消息和返回给web客户端的消息体。
play.api.mvc.SimpleResult 用于定义该类result:
def hello(name: String) = Action { Ok("Hello " + name) }
当然,也有一些助手方法用于方便的创建常用的result,如 Ok result:
def index = Action { Ok("Hello world!") }
该代码产生和上例类似的响应。
下面展示了创建不同 Results 的示例。
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 的traint和companion对象查看全部助手方法。
重定向也是普通Result
浏览器重定向仅仅是另一种普通响应。但是,此类返回值不携带响应体。
有几种创建重定向的方法:
def index = Action { Redirect("/user/home") }
默认使用 303 SEE_OTHER 响应类型,但你也可以按需设置其他状态字:
def index = Action { Redirect("/user/home", status = MOVED_PERMANENTLY) }
“TODO” 虚拟页面
你可以使用一个Action的空实现定义为TODO:它的result是个标准的 'Not implemented yet'页面:
def index(name:String) = TODO
内建的HTTP路由
Router是將每个接受到的HTTP请求转换成Action调用的组件。
一个HTTP请求,被框架视为一个事件。该事件包含了两类重要信息:
请求路径(例如:/clients/1542,/photos/list),和查询参数。
HTTP方法(GET,PUT,POST...)
路由规则在conf/routes中定义,并被编译。意味着,你可以在浏览器中直接查看路由错误:
routes声明语法
conf/routes配置文件被router解析使用。该文件定义了应用程序的所有路由规则。每个路由定义包含HTTP方法,URI模式,和一个Action调用。
先看看示例:
GET /clients/:id controllers.Clients.show(id: Long)
每个路由定义都以一个HTTP方法开头,仅接URI模式,最后是Action调用定义。
# Display a client. GET /clients/:id controllers.Clients.show(id: Long)
可以使用 # 编写注释
# Display a client. GET /clients/:id controllers.Clients.show(id: Long)
HTTP方法
HTTP方法可以是任何HTTP支持的方法(GET,POST,PUT,DELETE,HEAD)。
URI模式
URI模式定义了路由的请求路径。部分路径可以是动态的。
静态路径
例如,想精确的匹配接受的GET /clients/all 请求,可以这样定义:
GET /clients/all controllers.Clients.list()
动态部分
如果你想定义一个通过ID检索用户的路由,你就需要添加一个动态部分:
GET /clients/:id controllers.Clients.show(id: Long)
需要注意的是一个URI模式可以定义多个动态部分。
动态部分的默认匹配策略被正则式 [^/]+ 定义,意味着任何定义了 :id 的动态部份都将被完全匹配。
跨越多个 /
如果你想捕获多个动态部分,被斜线分隔,你可以使用 *id 语法定义动态部分,它將使用 .* 正则规则:
GET /files/*name controllers.Application.download(name)
这里,类似/files/images/logo.png这样的GET请求,name动态部分將捕获images/logo.png值。
使用正则式定义动态部分
你也可以使用正则式定义动态部分,利用 $id<regex>语法:
GET /clients/$id<[0-9]+> controllers.Clients.show(id: Long)
路由的最后一部分定义Action调用。这部分必须定义一个经验证返回值为 play.api.mvc.Action 值的控制器方法的调用声明。
如果该方法未定义任何参数,请给出方法全限定名:
GET / controllers.Application.homePage()
如果action方法定义了一些参数,所有这些参数將在请求的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)
以下是相应的controller show 方法的定义:
def show(page: String) = Action { loadContentFromDatabase(page).map { htmlContent => Ok(htmlContent).as("text/html") }.getOrElse(NotFound) }
参数类型
对于String类型的参数,输入参数是可选的。如果你要玩改造,传入一个特定Scala类型的参数,明确指定:
GET /client/:id controllers.Clients.show(id: Long)
并相应在控制器show方法中定义。controllers.Clients:
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)
路由优先级
许多URL路径都可满足匹配要求。如果有冲突,采用先声明先使用的原则。
反转路由
router 可以將一个Scala方法调用反转生成URL。这使得你能將所有的URI模式在单一文件中集中配置,这样你就能更自信的將来重构应用。
配置文件使用的每个控制器,router都将在 routes 包中生成一个 “反转的” 控制器,它具有相同的方法相同的签名,但使用play.api.mvc.Call代替play.api.mvc.Action做为返回值。
在play.api.mvc.Call定义HTTP调用,并提供HTTP方法和URI。
例如,如果你像这样创建控制器:
package controllers import play.api._ import play.api.mvc._ object 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 反转出 hello 方法的URL:
// Redirect to /hello/Bob def helloBob = Action { Redirect(routes.Application.hello("Bob")) }
Result 类型將根据设定的Scala值自动推断。
例如:
val textResult = Ok("Hello World!")
将Content-Type自动设置为text/plain,而:
val xmlResult = Ok(<message>Hello World!</message>)
会將 Content-Type 设为 text/xml.
提示:这是通过 play.api.http.ContentTypeOf 类来完成的。
该机制相当有用,但有时候你需要定制。可以使用as(contentType)实现:
val htmlResult = Ok(<h1>Hello World!</h1>).as("text/html")
更好的方式:
val htmlResult = Ok(<h1>Hello World!</h1>).as(HTML)
注意:使用 HTML 替代 "text/html"的好处是字符编码转被自动处理,并且Content-Type头也会被设为 text/html;charset=utf-8。我们稍后会看到。
你可以为响应结果添加(更新)HTTP头信息。
Ok("Hello World!").withHeaders( CACHE_CONTROL -> "max-age=3600", ETAG -> "xx" )
注意设置HTTP请求头将自动覆盖现有值。
设置和删除Cookies
Cookies不过是HTTP HEAD的特定部分,不过我们提供了一系列的便利处理方法。
你可以轻松的给HTTP Response 添加Cookie:
Ok("Hello world").withCookies( Cookie("theme", "blue") )
删除浏览器Cookie:
Ok("Hello world").discardingCookies("theme")
对于HTTP响应,确保正确的字符编码非常重要。Play默认使用utf-8处理编码。
字符集编码既用来將响应文本转换成相应的网络socket字节码,也用于确定HTTP头 ;charset=xxx 的信息。
字符集编码由 play.api.mvc.Codec 自动处理。在当前请求上下文中导入 一个隐式 play.api.mvc.Codec 对象,可以改变字符集,以供所有操作使用:
object 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 编码,也自动生成 text/html;charset=iso-8859-1 Content-Type头信息。
现在,想知道 HTML 方法是怎么工作的吗?以下就是该方法的定义:
def HTML(implicit codec: Codec) = { "text/html; charset=" + codec.charset }
你也可以在你的API用类似的方式处理字符编码。
它们在Play中有何不同?
如果你试图在多个HTTP请求中保存数据,你可以將它们保存在Session或Flash中。保存在Session中的数据,对整个用户会话都有效,而保存在Flash中的数据只对下一次请求有效。
理解Session和Flash的数据不在服务器端保存,而由客户cookie维护是相当重要的。这意味着数据容量非常有限(最大4KB),并且你只能保存string值。
当然cookie数据被安全码加密,因此客户端不能修改该数据(或使其失效)。
Play Session 不是为缓存数据准备的。如果你想缓存某个Session相关的数据,你可以使用Play内建的缓存机制,保存唯一的SessionID值,维护用户数据。
Session没有超时技术。当用户关闭浏览器时,它就会失效。如果你需要为特定的应用提供超时功能,可以在用户Session保存时间戳(timestamp),根据应用的需要来使用它。(如session最大生存时间,过期时间等等)
读取Session值
你可以通过request获取Session
def index = Action { request => request.session.get("connected").map { user => Ok("Hello " + user) }.getOrElse { Unauthorized("Oops, you are not connected") } }
另外,也可以通过一个隐式的request取得Session:
def index = Action { implicit request => session.get("connected").map { user => Ok("Hello " + user) }.getOrElse { Unauthaurized("Oops, you are not connected") } }
向Session存储数据
因为Session仅仅是个Cookie,也仅仅是一个HTTP请求头。你可以像操纵其它Result属性一样的操纵Session数据:
Ok("Welcome!").withSession( "connected" -> "[email protected]" )
需要注意该方式將替换整个session。下面是对现有session添加元素的方式:
Ok("Hello World!").withSession( session + ("saidHello" -> "yes") )
可用类似的方式删除数据:
Ok("Theme reset!").withSession( session - "theme" )
丢弃整个session
下面是一个特别的操作,將丢弃整个session
Ok("Bye").withNewSession
Flash 上下文
Flash上下文的工作机制与Session很像,但有两点不同:
只为一个请求保存数据
Flash Cookie未特别标识,它可能会被用户修改
重要:Flash 上下文只应用在非ajax请求的普通应用中,用来传输类似success/error的消息。因为数据仅保存到下一次请求,又因在复杂的应用中无法担保请求顺序,Flash会受竞争条件影响。
下面是使用 Flash scope 的例子:
def index = Action { implicit request => Ok { flash.get("success").getOrElse("Welcome!") } } def save = Action { Redirect("/home").flashing( "success" -> "The item has been created" ) }
Body Parser是什么
HTTP PUT 或 POST 请求包含着body。body可以用Content-Type指定格式。在Play中, body parser 將请求体转换成Scala值。
然而body可能很大,body parser 不能等待数据全部加载到内存后再解析。 A BodyParser [A] 基本上算是一个Iteratee [Array[Byte],A],意味着它以块为单位接收字节数据(只要浏览器上传一些数据),并且以 A 类型计算结果值.
先考虑几个例子:
一个 text body parser 收集字节块,转成String,將该String值做为返回值(Iteratee [Array[Byte],String])
一个 file body parser 可將每份数据块保存到一个本地文件中,并给予一个java.io.File引用作为返回值(Iteratee [Array[Byte],File])
A s3 body parser 可以將每一块字节推送到Amazon S3,將S3 object id做为返回值(Iteratee [Array[Byte],S3ObjectId ])
另外,一个 body parser可以在解析开始前,对HTTP头做些预先检查。例如:body parser可以检查一些HTTP头是否被正确设置,或者用户是否试图上传过大文件等。
注意:这就是为什么 body parser 不是一个真正的 Iteratee [Array[Byte],A] 的原因,但又恰恰因为是一个[Array[Byte],Either[Result,A]],意味着,它有权直接发回HTTP响应结果(通常是400 BAD_REQUEST , 412 PRECONDITION _FAILED or 413 REQUEST _ENTITY_TOO_LARGE),如果它觉得不能为 request body 计算正确的值的话。
一旦 body parser完成工作并返回一个A类型的值,相应的action函数將被调用,经处理的body值就被传递给request。
之前,我们提到一个Action是一个 Request => Result 函数。这不完全正确。
让我们更细致的查看 Action trait:
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 是request body 的类型。我们可以使用任意Scala类型指定,例如 String,NodeSeq,Array[Byte],JsonValue,或者java.io.File,只要我们有一个可以处理该类型的body parser。
总而言之,一个 Action[A] 使用一个 BodyParser[A] 从HTTP请求中,取出一个A类型的值,并构建一个Request[A]对象,转递给action代码。
之前的例子中,我们从未指定 body parser。那么,它是怎么工作的?如果你不指定 body parser,Play將使用默认的,会將request body 处理为一个 play.api.mvc.AnyContent的 body parser。
该 body parser 检查Content-Type,以决定处理为何种类型的值:
text/plain:String
application/json:JsValue
text/xml:NodeSeq
application/form-url-encoded:Map[String,Seq[String]]
multipart/form-data:MultipartFormData[TemporaryFile]
任何其它类型:RawBuffer
例如:
def save = Action { request => val body: AnyContent = request.body val textBody: Option[String] = body.asText // Expecting text body textBody.map { text => Ok("Got: " + text) }.getOrElse { BadRequest("Expecting text/plain request body") } }
body parser 的定义位于play.api.mvc.BodyParsers.parse包下。
例如,创建一个期望text body的action(正如前面的例子):
def save = Action(parse.text) { request => Ok("Got: " + request.body) }
看到代码有多简单了吗?不处理错误,因为parse.text body parser本身就会根据错误发送400 BAD_REQUEST响应。我们不需在代码中重复检查,我们可以放心的假定request.body包含了经验证的 String body。
我们也可以使用:
def save = Action(parse.tolerantText) { request => Ok("Got: " + request.body) }
该代码并未检查Content-Type,并且常常以String加载body。
提示:
Tip: There is a tolerant fashion provided for all body parsers included in Play.
这是另一个例子,我们將 request body 保持在一个文件中:
def save = Action(parse.file(to = new File("/tmp/upload"))) { request => Ok("Saved the request content to " + request.body) }
之前的例子,所有的 request body 都存储在同一文件中。这会有些问题,不是吗?让我们编写另一个自定义 body parser 从Session中提取用户名,为每个用户分配一个文件:
val storeInUserFile = parse.using { request => request.session.get("username").map { user => file(to = new File("/tmp/" + user + ".upload")) }.getOrElse { error(Unauthorized("You don't have the right to upload here")) } } def save = Action(storeInUserFile) { request => Ok("Saved the request content to " + request.body) }
注意:这里我们并没有编写自己的 Body Parser,仅仅是结合现有的。这通常都足够了,它已涵盖了大多数情况。编写一个全新的 Body Parser会在高级主题中提到。
基于文本的 body parser(如text,json,xml或者formUrlEncoded)会使用最大内容长度,因为内容必须全部加载到内存中。
默认最大长度为100KB,但你也可以内嵌指定:
// Accept only 10KB of data. def save = Action(parse.text(maxLength = 1024 * 10)) { request => Ok("Got: " + text) }
提示:最大内容长度可以在application.conf中设置:
parsers.text.maxLength=128K
你也可以用 maxLength 包装任何的 body parser:
// Accept only 10KB of data. def save = Action(maxLength(1024 * 10, parser = storeInUserFile)) { request => Ok("Saved the request content to " + request.body) }
本章介绍一些通用的action功能。
让我们以一个简单的日志装饰功能起步:我们想记录该action的每次调用。
第一种方法,不定义自己的Action,仅提供一个助手方法构建标准的Action:
def LoggingAction(f: Request[AnyContent] => Result): Action[AnyContent] = { Action { request => Logger.info("Calling action") f(request) } }
可以这么使用:
def index = LoggingAction { request => Ok("Hello World") }
示例很简单,但它仅适用于默认的 parse.anyContent body parser,我们没办法指定自定义的 body parser。我们当然可以定义另一个助手方法:
def LoggingAction[A](bp: BodyParser[A])(f: Request[A] => Result): Action[A] = { Action(bp) { request => Logger.info("Calling action") f(request) } }
接着:
def index = LoggingAction(parse.text) { request => Ok("Hello World") }
另一种方式是自定义LogginAction,作为其它Action的包装者:
case class Logging[A](action: Action[A]) extends Action[A] { def apply(request: Request[A]): Result = { Logger.info("Calling action") action(request) } lazy val parser = action.parser }
现在你可以用它包装任何action:
def index = Logging { Action { Ok("Hello World") } }
注意:它將重用包装过的action body parser,你也可以编写:
def index = Logging { Action(parse.text) { Ok("Hello World") } }
另一种不定义Loggin类而完成同样工作的方式:
def Logging[A](action: Action[A]): Action[A] = { Action(action.parser) { request => Logger.info("Calling action") action(request) } }
让我们看一个更复杂而常见的认证例子。主要问题是我们需要一个能放行已认证用户,能包装action和body parse,并扮演用户认证的action。
def Authenticated[A](action: User => Action[A]): Action[A] = { // Let's define an helper function to retrieve a User def getUser(request: RequestHeader): Option[User] = { request.session.get("user").flatMap(u => User.find(u)) } // Wrap the original BodyParser with authentication val authenticatedBodyParser = parse.using { request => getUser(request).map(u => action(u).parser).getOrElse { parse.error(Unauthorized) } } // Now let's define the new Action Action(authenticatedBodyParser) { request => getUser(request).map(u => action(u)(request)).getOrElse { Unauthorized } } }
你可以这么使用:
def index = Authenticated { user => Action { request => Ok("Hello " + user.name) } }
注意:在play.api.mvc.Security.Authenticated包中,已经有一个比该例更好的实现了。
让我们看看不包装整个action,不携带认证的body parser,如何重写前一个例子:
def Authenticated(f: (User, Request[AnyContent]) => Result) = { Action { request => request.session.get("user").flatMap(u => User.find(u)).map { user => f(user, request) }.getOrElse(Unauthorized) } }
这样使用:
def index = Authenticated { (user, request) => Ok("Hello " + user.name) }
面对的问题是,你不再能标记request为implicit。但你可以使用柯里化来解决:
def Authenticated(f: User => Request[AnyContent] => Result) = { Action { request => request.session.get("user").flatMap(u => User.find(u)).map { user => f(user)(request) }.getOrElse(Unauthorized) } }
接下你可以:
def index = Authenticated { user => implicit request => Ok("Hello " + user.name) }
另一种方式(可能是最简单的)是创建自定义request子类,如 AuthenticatedRequest (我们已將两个参数合并为一个参数):
case class AuthenticatedRequest( val user: User, request: Request[AnyContent] ) extends WrappedRequest(request) def Authenticated(f: AuthenticatedRequest => Result) = { Action { request => request.session.get("user").flatMap(u => User.find(u)).map { user => f(AuthenticatedRequest(user, request)) }.getOrElse(Unauthorized) } }
接着:
def index = Authenticated { implicit request => Ok("Hello " + request.user.name) }
我们当然可以按需扩展该例子使其更通用,让其可以指定一个body parser。
case class AuthenticatedRequest[A]( val user: User, request: Request[A] ) extends WrappedRequest(request) def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = { Action(p) { request => request.session.get("user").flatMap(u => User.find(u)).map { user => f(AuthenticatedRequest(user, request)) }.getOrElse(Unauthorized) } } // Overloaded method to use the default body parser import play.api.mvc.BodyParsers._ def Authenticated(f: AuthenticatedRequest[AnyContent] => Result): Action[AnyContent] = { Authenticated(parse.anyContent)(f) }