该API直接位于 play 顶级包中(而play.mvc是为Java开发者提供的)。对于Scala开发者,查阅play.api.mvc。html
Actions, Controllers and Results
什么是Action?
大多数Play应用程序接受的请求由一个Action处理。
一个play.api.mvc.Action基本上是 一个 (play.api.mvc.Request => play.api.mvc.Result)函数,它处理请求并生成响应发给客户端。
java
val echo = Action { request => Ok("Got request [" + request + "]") }
action返回一个 play.api.mvc.Result对象,使用 HTTP response 对象返回给客户端。例如: Ok 返回一个200响应,包含text/plain 响应体。
建立Action
最简单的Action仅须要定义一个参数,一个表达式块,返回一个Result值web
Action { Ok("Hello world") }
这是建立Action最简单的方式,但咱们没法获取request对象。一般Action中都须要访问request对象。
看看第二个Action,包含了参数Request => Result :ajax
Action { request => Ok("Got request [" + request + "]") }
标记 request 参数为 隐式 一般都颇有用,可供其它API隐式的使用:json
Action { implicit request => Ok("Got request [" + request + "]") }
最后一种建立方式,包含了一个特别的可选 BodyParser 参数:
api
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" -> "user@gmail.com" )
须要注意该方式將替换整个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) }