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