概述
Play 框架是一個完整的 Web 應(yīng)用開發(fā)框架,覆蓋了 Web 應(yīng)用開發(fā)的各個方面。Play 框架在設(shè)計的時候借鑒了流行的
Ruby on Rails 和 Grails 等框架,又有自己獨(dú)有的優(yōu)勢。使用 Play 框架可以方便和高效的開發(fā)出 Java Web
應(yīng)用。通過 Play 框架提供的命令行工具,可以快速的創(chuàng)建出一個 Web 應(yīng)用的基本骨架。它的 Java
代碼動態(tài)編譯機(jī)制,使得修改代碼之后,不需要重啟服務(wù)器就可以直接看到修改之后的結(jié)果,調(diào)試起來非常方便。它使用 JPA
規(guī)范來完成領(lǐng)域?qū)ο蟮某志没?,可以很方便的使用不同的關(guān)系數(shù)據(jù)庫作為后臺存儲。使用 Play 框架可以很容易的構(gòu)建使用 REST
架構(gòu)風(fēng)格的應(yīng)用。它使用 Groovy 作為視圖層模板使用的表達(dá)式語言。模板之間的繼承機(jī)制也可以避免代碼的重復(fù)??偟膩碚f,Play
框架非常適合快速 Web 應(yīng)用開發(fā)。
Play 框架采用經(jīng)典的 MVC 架構(gòu),把 Web 應(yīng)用分成模型層、控制層和視圖層三個層次。每個層次對應(yīng)的文件被存放在不同的目錄下面,方便組織和管理。使用 Play 框架的 Web 應(yīng)用具有相同的目錄結(jié)構(gòu),如 圖 1 所示。
圖 1. 使用 Play 框架的 Web 應(yīng)用的目錄結(jié)構(gòu)
如 圖 1 所示,應(yīng)用自身的文件被放在 app
目錄下面,三個子目錄分別存放的是 MVC 模式的三個層次的內(nèi)容。其中 models
和 controllers
目錄下面是 Java 源文件,而 views
目錄下面則是視圖層使用的模板文件。conf
目錄下面存放的是應(yīng)用的配置文件、HTTP 路由文件和國際化所需的消息文件。public
目錄則是存放 Web 應(yīng)用的靜態(tài)文件,包括 JavaScript、CSS 和圖像文件等。lib
目錄存放所需的額外的 Java 庫。test
目錄存放的是測試結(jié)果。
開發(fā)環(huán)境
本文中使用的 Play 框架的版本是 1.0.3.1,使用的集成開發(fā)環(huán)境是 Eclipse 3.6, 使用 Dojo 作為 JavaScript 框架。在 Play 框架官方網(wǎng)站(見 參考資料)下載 Play 框架的壓縮包之后,解壓到某個目錄,并把該目錄下面的 bin
目錄添加到環(huán)境變量中。接著啟動一個命令窗口,運(yùn)行 play new developers_notebook
就可以創(chuàng)建出一個新的名為 developers_notebook
的 Web 項目。在項目目錄的父目錄下面,運(yùn)行 play eclipsify developers_notebook
就可以創(chuàng)建出來 Eclipse 工程。通過 Eclipse 導(dǎo)入此工程就可以在 Eclipse 里面進(jìn)行開發(fā)了。Play 框架的 support
目錄下的 eclipse
目錄下有個名為 org.playframework.playclipse
的 Eclipse 插件,將此插件復(fù)制到 Eclipse 的 plugins
目錄就可以安裝。運(yùn)行 play run
就可以運(yùn)行此 Web 應(yīng)用,訪問 http://localhost:9000
就可以看到。每次在 Eclipse 里面修改了代碼之后,不需要重新啟動應(yīng)用,只需要刷新頁面就能看到更新之后的結(jié)果。這是 Play 框架的一個非常方便的特性。
本文中的示例應(yīng)用稱為“開發(fā)人員記事本”。開發(fā)人員可以用它來記錄開發(fā)過程中的一些注意事項。下面首先介紹 Play 框架中的模型層。
回頁首
模型層
模型層包含的是 Web 應(yīng)用中的領(lǐng)域?qū)ο?。Play 框架推薦的實踐是模型層的對象不應(yīng)該是僅包含 getter/setter
方法的簡單 Java Beans,而應(yīng)該有自己的業(yè)務(wù)邏輯。Play 框架中應(yīng)用的模型層類可以是任何的 Java 類。與一般的 Java
Beans 不同的是,模型層類使用聲明為 public
的域作為對象的屬性。Play 框架會自動生成相應(yīng)的 getter/setter 方法。這樣可以使得代碼更加簡潔。開發(fā)人員也可以提供自己的 getter/setter 方法實現(xiàn)。
領(lǐng)域?qū)ο蟪志没?/span>
領(lǐng)域?qū)ο蟮膶嵗话阈枰志没聛?。最常見的持久化方式就是使用關(guān)系數(shù)據(jù)庫。Play 框架使用 JPA
規(guī)范來進(jìn)行領(lǐng)域?qū)ο蟮某志没?。具體的后臺實現(xiàn)使用的是 Hibernate。開發(fā)人員只需要使用 JPA
規(guī)范定義的標(biāo)注,就可以聲明領(lǐng)域的持久化行為。比較好的做法是將領(lǐng)域?qū)ο箢惱^承自 Play 框架提供的 play.db.jpa.Model
類。play.db.jpa.Model
類提供了一個域 id
作為對象的標(biāo)識符,也是對應(yīng)的數(shù)據(jù)庫表中的主鍵。play.db.jpa.JPASupport
類是 play.db.jpa.Model
的父類,提供了一些實用方法用來完成從領(lǐng)域?qū)ο蟮綌?shù)據(jù)庫之間的映射。表 1 中列出了一些重要的方法,包括常用的增刪改查操作。
表 1. play.db.jpa.JPASupport API 說明
方法 | 說明 |
create(type, name, params)
|
用來創(chuàng)建領(lǐng)域?qū)ο箢惖囊粋€實例。參數(shù) type 表示的是領(lǐng)域?qū)ο箢?,類型?java.lang.Class ;name 表示的是領(lǐng)域?qū)ο箢惖拿Q;params 表示的是一個包含了實例中屬性值的類型為 java.util.Map<java.lang.String,java.lang.String[]> 的哈希表。 |
edit(obj, name, params)
|
用來編輯領(lǐng)域?qū)ο箢惖囊粋€實例。參數(shù) obj 表示的是領(lǐng)域?qū)ο髮嵗?;參?shù) name 和 params 的含義與 create() 方法的相同。 |
delete()
|
用來刪除單個領(lǐng)域?qū)ο箢惖膶嵗?/td>
|
delete(query, params)
|
用來刪除多個領(lǐng)域?qū)ο箢惖膶嵗?。參?shù) query 表示的是檢索待刪除實例的查詢,而 params 表示的是查詢所使用的參數(shù)。 |
deleteAll()
|
用來刪除領(lǐng)域?qū)ο箢惖乃袑嵗?/td>
|
find(query, params)
|
用來查找領(lǐng)域?qū)ο箢惖膶嵗?。參?shù) query 表示的是查找時所用的查詢,而 params 表示的是查詢所使用的參數(shù)。 |
findAll()
|
用來查找領(lǐng)域?qū)ο蟮乃袑嵗?/td>
|
findById(id)
|
用來根據(jù)標(biāo)識符查找領(lǐng)域?qū)ο蟮膶嵗?/td>
|
count(query, params)
|
用來計算某個查詢結(jié)果中包含的領(lǐng)域?qū)ο蟮膶嵗龜?shù)。參數(shù) query 和 params 的含義與 find() 方法相同。 |
save()
|
用來保存該領(lǐng)域?qū)ο髮嵗綌?shù)據(jù)庫中。 |
all()
|
用來查找系統(tǒng)中的全部領(lǐng)域?qū)ο蟮膶嵗?/td>
|
表 1 中列出的方法中,find()
和 all()
方法的返回值是 play.db.jpa.JPASupport.JPAQuery
類的實例,表示一個領(lǐng)域?qū)ο髮嵗牟樵兘Y(jié)果。對于此查詢結(jié)果,可以進(jìn)行進(jìn)一步的操作。具體的操作,如 表 2 所示。
表 2. play.db.jpa.JPASupport.JPAQuery API 說明
方法 | 說明 |
bind(name, param)
|
用來綁定一個參數(shù)的實際值到查詢上。在查詢語句中可以使用形式參數(shù)作為占位符,參數(shù)的實際值可以通過此方法來指定。 |
fetch()
|
用來獲取此查詢的所有記錄。 |
fetch(max)
|
用來獲取此查詢的前面 max 條記錄。 |
fetch(page, length)
|
用來對查詢結(jié)果進(jìn)行分頁。參數(shù) page 表示當(dāng)前的頁數(shù),從 1 開始;length 表示每頁的記錄數(shù)。 |
first()
|
用來返回查詢結(jié)果中的第一條記錄 |
from(position)
|
用來設(shè)置查詢結(jié)果中處理的起始位置。參數(shù) position 表示起始位置的序號。該方法的返回結(jié)果是一個新的 play.db.jpa.JPASupport.JPAQuery 對象。 |
使用 表 2 中給出的方法,就可以在領(lǐng)域?qū)ο箢愔刑砑右恍┓浅嵱玫姆椒?,而不需要把這些方法添加到額外的服務(wù)層中。在示例應(yīng)用中,Note
這個領(lǐng)域?qū)ο箢惐硎镜氖怯脩籼砑拥挠涗洝?a >代碼清單 1 中給出了 Note
類中的一些實用方法。
清單 1. 領(lǐng)域?qū)ο箢愔械膶嵱梅椒?/strong>
// 創(chuàng)建新的領(lǐng)域?qū)ο?Note 的實例,edit() 方法的使用與 create() 類似
Map<String, String[]> params = new HashMap<String, String[]>();
params.put("note.title", new String[] {"My note"});
params.put("note.content", new String[] {"My note's content"});
Note.create(Note.class, "note", params).save();
// 使用 find() 來進(jìn)行查找
List<Note> notes = Note.find("byTitle", "My note").fetch();
// 使用 findById() 來查找單個實例
Note note1 = Note.findById(1);
// 使用 delete() 來刪除對象實例
Note.delete("byTitle", "My note");
// 返回查詢結(jié)果中的第 2 到第 11 條記錄。
Note.find("byTitle", "My note").from(1).fetch(10);
|
在介紹完 Play 框架的模型層之后,下面介紹控制層。
回頁首
控制層
Play 框架中的控制層是模型層和視圖層之間的橋梁。控制層負(fù)責(zé)接收 HTTP
請求并返回相應(yīng)的響應(yīng)。一般來說,控制層的典型實現(xiàn)是接收到 HTTP
請求之后,從請求中獲取一些參數(shù),再調(diào)用服務(wù)層對應(yīng)的處理方法。服務(wù)層的方法會對領(lǐng)域?qū)ο筮M(jìn)行操作,完成具體的業(yè)務(wù)邏輯。最后,某種格式的響應(yīng)被返回給請
求者,如 HTML 頁面、JSON 數(shù)據(jù)和 XML 數(shù)據(jù)等。Play 框架的控制層實現(xiàn)使得完成這樣的典型場景變得非常簡單。
Play 框架中的每個控制器都是一個普通的 Java 類,繼承自 play.mvc.Controller
類,在包 controllers
中??刂破黝愔械拿總€公開的靜態(tài)方法都表示一個動作。每個動作負(fù)責(zé)完整的請求 / 響應(yīng)的流程,也就是說,所有前面提到的所有請求/響應(yīng)的過程都需要在每個動作中來完成。
參數(shù)綁定
在控制層實現(xiàn)中很繁瑣但是必不可少的操作就是解析 HTTP 請求中的參數(shù)。不同的 Web 開發(fā)框架會提供自己的參數(shù)解析方式。Play
框架也提供了相應(yīng)的支持。Play 框架可以解析 HTTP 請求中查詢字符串和 URI
路徑中包含的以及請求體中以格式編碼的參數(shù)。所有這些參數(shù)都放在 params
對象中,其中包含 get()
、getAll()
和 put()
等方法用來獲取和設(shè)置參數(shù)的值。除了這種傳統(tǒng)的使用方式之外,Play 框架還支持直接把參數(shù)的值綁定到動作方法的參數(shù)上面。比如一個動作方法的聲明是 show(String username)
,那么請求中的參數(shù) username
的值會在 show()
方法被調(diào)用時作為實際參數(shù)傳遞進(jìn)去。Play 框架會負(fù)責(zé)完成相應(yīng)的類型轉(zhuǎn)換。值得一提的是對于日期類型(java.util.Date
)的參數(shù),Play 框架支持多種類型的日期格式的轉(zhuǎn)換。比如動作方法的聲明是 display(Date postedAt)
,而請求的格式可能是 /display?postedAt=2010-09-22
,Play 框架會自動完成相應(yīng)的類型轉(zhuǎn)換。
除了常見的基本數(shù)據(jù)類型之外,Play 框架還支持直接綁定領(lǐng)域?qū)ο蟮膶嵗?。比如動作方法的聲明?create(Note note)
,可以在參數(shù)中直接指定對象實例的屬性的值。請求的格式可能是 /create?title=Note123&content=Good
。Play 框架會負(fù)責(zé)創(chuàng)建一個 Note
類的實例,并根據(jù)參數(shù)的值設(shè)置該實例的屬性 title
和 content
的值。這種綁定方式不僅支持簡單對象,還支持嵌套對象和列表。比如 /create?tags[0]=ajax&tags[1]=web
可以設(shè)置列表類型屬性 tags
的值。
Play 框架的這種綁定方式還支持文件對象,使得上傳文件變得非常簡單。只需要在表單中添加文件上傳的控件(<input type="file">
)并使用 multipart/form-data
編碼來提交請求,在動作方法的參數(shù)中就可以獲取到上傳文件對應(yīng)的 java.io.File
對象。比如動作方法的聲明可能是 upload(File picture)
。上傳的文件被保存在臨時目錄中,在請求完成之后會被自動刪除??梢栽趧幼鞣椒ㄖ型瓿蓪ι蟼魑募牟僮鳌?/p>
返回響應(yīng)結(jié)果
在控制層的動作方法完成了與業(yè)務(wù)邏輯相關(guān)的處理之后,需要把響應(yīng)返回給客戶端。響應(yīng)的結(jié)果可能是正確完成,也可能是出現(xiàn)錯誤。Play 框架提供了方便的實現(xiàn)用來返回不同類型的響應(yīng)。使用 play.mvc.Controller
類提供的不同方法就可以生成這些響應(yīng)內(nèi)容。
- 請求正確完成,HTTP 狀態(tài)代碼為 200。使用
ok()
方法生成不帶內(nèi)容的響應(yīng)。使用 render()
方法來生成使用模板的響應(yīng)。使用 renderText()
方法生成 text/plain
類型的純文本響應(yīng)。使用 renderXml()
方法生成 text/xml
類型的 XML 格式的響應(yīng)。使用 renderJSON()
方法生成 application/json
類型的 JSON 格式的響應(yīng)。使用 renderBinary()
方法生成二進(jìn)制內(nèi)容的響應(yīng)。
- 跳轉(zhuǎn)到新的頁面,HTTP 狀態(tài)代碼為 3XX。使用
redirect()
方法來跳轉(zhuǎn)到新的 URL。使用 notModified()
方法來返回狀態(tài)代碼 304。
- HTTP 狀態(tài)代碼 4XX。使用
unauthorized()
方法返回狀態(tài)代碼 401。使用 forbidden()
方法返回狀態(tài)代碼 403。使用 notFound()
方法返回狀態(tài)代碼 404。
- 服務(wù)器內(nèi)部錯誤,HTTP 狀態(tài)代碼 5XX。使用
error()
方法返回狀態(tài)代碼 500。
從上面列出的方法可以看出,Play 框架使用一些有意義的方法名稱替換掉了難以記憶的 HTTP 狀態(tài)代碼,使用起來更加方便。同時,對于常見的響應(yīng)格式,包括 HTML、XML、JSON 和二進(jìn)制內(nèi)容,都提供了相應(yīng)的方法,使得開發(fā)人員不會遺漏掉響應(yīng)中 Content-Type
的聲明。
方法攔截
控制層的方法通常需要執(zhí)行一些橫切的邏輯,比如用戶認(rèn)證、加載通用信息和記錄日志等。在 Spring
框架中,這些橫切的邏輯是通過面向方面編程(AOP)的支持來實現(xiàn)的。Play
框架提供了更加簡單易用的方法攔截支持,通過簡單的標(biāo)注就可以定義一些執(zhí)行攔截操作的方法。這些方法必須非公開的靜態(tài)方法。Play
框架支持的方法攔截標(biāo)注有 @Before
、@After
、@Finally
和 @With
等四種。
用 @Before
標(biāo)注的方法在動作方法執(zhí)行之前被調(diào)用。@After
標(biāo)注的方法在動作方法執(zhí)行之后被調(diào)用。@Finally
標(biāo)注的方法在動作方法的響應(yīng)結(jié)果已經(jīng)成功生成之后被調(diào)用。這三個標(biāo)注都支持額外的兩個屬性:priority
表示標(biāo)注的方法的優(yōu)先級,0 為最高;unless
是一個字符串?dāng)?shù)組,表示不適用此攔截方法的動作方法的名稱。如 @Before(unless="index")
表示此攔截方法不會應(yīng)用在動作方法 index()
上。
如果控制器類中存在繼承體系結(jié)構(gòu)的話,父類中聲明的攔截方法對于所有子類的動作方法都是適用的。在有些情況下,開發(fā)人員可能希望把攔截方法定
義在不同的類體系結(jié)構(gòu)中。由于 Java 不支持多繼承,無法通過繼承的方式來應(yīng)用來自不同類體系結(jié)構(gòu)上的攔截方法。針對這種情況,Play 框架提供了
@With
標(biāo)注。在控制器類 ControllerA
中定義的攔截方法可以通過 @With
標(biāo)注來應(yīng)用到另外一個控制器類 ControllerB
上,而且不通過繼承方式來實現(xiàn)。只需要在 ControllerB
中聲明 @With(ControllerA.class)
即可。
在介紹完 Play 框架的控制層之后,下面介紹視圖層。
回頁首
視圖層
Web 開發(fā)框架的使用者都習(xí)慣于使用某種模板技術(shù)來生成 HTML 頁面,這些技術(shù)包括常見的 JSP、ASP 和 PHP 等。Play
框架也提供了自己的模板技術(shù),可以用來動態(tài)的創(chuàng)建 HTML、XML、JSON 以及其它文本類型的內(nèi)容。Play 框架的模板技術(shù)使用的是
Groovy 語言。Groovy 語言的靈活性和簡潔性使得 Play
框架的模板簡單而且易用。在模板中可以混用靜態(tài)內(nèi)容和生成動態(tài)內(nèi)容的各種元素。在模板中可以使用的動態(tài)元素如 表 3 所示。
表 3. 模板中可用的動態(tài)元素
動態(tài)元素 | 說明 |
${...}
|
用來對一個表達(dá)式進(jìn)行求值。如 ${note.title} 的值是領(lǐng)域?qū)ο?note 的屬性 title 的值。 |
@{...} 和 @@{...}
|
用來生成調(diào)用控制器中動作方法的 URL,可以用在頁面的鏈接中。@{...} 和 @@{...} 生成的分別是相對 URL 和絕對 URL。如 <a href="@{Application.index()}"> 首頁 </> 生成一個指向首頁的鏈接。 |
&{...}
|
用來顯示經(jīng)過國際化之后的消息內(nèi)容。 |
*{...}*
|
用來添加注釋。如 *{ 這是注釋 }* 。 |
%{...}%
|
用來添加復(fù)雜的 Groovy 腳本,可以聲明變量和添加語句。 |
#{...}
|
用來調(diào)用 Play 框架的或是開發(fā)人員自定義的標(biāo)簽。 |
Play 框架中的標(biāo)簽的作用相當(dāng)于 JSP 中的標(biāo)簽。Play 框架本身提供一些常用的標(biāo)簽,開發(fā)人員也可以根據(jù)需要開發(fā)自己的標(biāo)簽。Play 框架內(nèi)置提供的標(biāo)簽說明如 表 4 所示。
表 4. Play 框架提供的標(biāo)簽
標(biāo)簽 | 說明 |
a
|
用來生成指向控制器中動作方法的 HTML 鏈接元素。如 #{a @Application.index()} 首頁 #{/a} 。 |
if 、ifnot 、elseif 和 else
|
用來進(jìn)行條件判斷。 |
set 和 get
|
用來設(shè)置和獲取可以在模板中使用的變量。如 #{set email:'alex@example.org'} 設(shè)置了變量 email 的值,可以通過 #{get 'email'} 來獲取。 |
script
|
用來生成一個 <script> 元素引用 /public/javascripts 目錄下的 JavaScript 文件。如 #{script 'dojo.js'} 引用 /public/javascripts/dojo.js 文件。 |
stylesheet
|
作用與 script 類似,不同的是使用 <link> 元素來引用 /public/stylesheets 目錄下的 CSS 文件。 |
list
|
用來遍歷一個對象集合。如 #{list items:notes, as:'note'} 用來遍歷對象集合 notes 。循環(huán)體中的每個對象用變量 note 來引用。在循環(huán)體的代碼中可以引用一些預(yù)定義的變量。這些變量有固定的名稱,但是會以循環(huán)體中的對象變量名作為前綴。在上面的例子中是 note_ 。如 note_index 表示當(dāng)前對象在集合中的序號;note_isFirst 表示是否是集合中的第一個對象;note_isLast 表示是否是集合中的最后一個對象;note_parity 表示當(dāng)前對象在集合中序號的奇偶值,可能是 even 和 odd 。 |
i18n
|
用來使得支持國際化的消息文件可以用 JavaScript 來訪問。在 JavaScript 代碼中可以 i18n() 方法來訪問。如 i18n('app_title') 。 |
errors
|
用來遍歷驗證錯誤的集合。使用方式與 list 類似,循環(huán)體中使用的對象變量名稱是 error 。 |
form
|
用來生成 HTML <form> 元素。 |
verbatim
|
禁用模板中的 HTML 轉(zhuǎn)義功能。 |
include
|
在當(dāng)前模板中引入另一個模板。如 #{include 'test.html'} 。 |
extends 和 doLayout
|
extends 使得當(dāng)前的模板繼承自另外一個模板。doLayout 用來在父模板中調(diào)用子模板。 |
在模板中可以使用來自不同地方的變量。首先是在模板生成的時候,由控制器中的動作方法通過 renderArgs
對象來添加的。如 renderArgs.put("username", "Alex")
就把一個變量 username
添加到了模板中。其次是一些隱含的變量,如 request
表示當(dāng)前的 HTTP 請求,session
表示當(dāng)前的會話,params
表示請求中的參數(shù)和 out
表示用來輸出響應(yīng)的 java.io.Writer
對象。最后就是可以通過 #{set}
來設(shè)置變量。
模板的繼承
Play 框架中可以使用 #{extends}
和 #{doLayout}
來實現(xiàn)模板之間的繼承。模板的繼承機(jī)制對于實現(xiàn)靈活的頁面布局很有幫助。一個模板可以定義清楚頁面的基本布局結(jié)構(gòu),其它模板可以繼承此模板并添加具體的內(nèi)容。這樣就可以避免在不同模板中重復(fù)相同的頁面元素。
在父模板中可以包含任意的內(nèi)容。在需要由子模板填充的位置,使用 #{doLayout /}
進(jìn)行聲明即可。在子模板中通過 #{extends}
來聲明所繼承的模板。如 #{extends 'main.html'}
就聲明繼承自模板 main.html
。當(dāng)子模板被生成之后,將包含父模板中的內(nèi)容。而子模板中只需要定義擴(kuò)展的內(nèi)容即可。
自定義標(biāo)簽
Play 框架自身提供的標(biāo)簽只能解決一些常見的需求,很多時候開發(fā)人員需要根據(jù)需要開發(fā)出自己的標(biāo)簽。一個標(biāo)簽的定義非常簡單,就是一個模板文件。模板文件被存放在 app/views/tags
目錄下,文件的名稱就是標(biāo)簽的名稱。在標(biāo)簽對應(yīng)的模板里面,開發(fā)人員可以添加任意的內(nèi)容。標(biāo)簽也是支持傳入?yún)?shù)的。在標(biāo)簽對應(yīng)的模板文件中可以用在參數(shù)名稱前面加上 _
的方式來引用參數(shù)的值。比如一個標(biāo)簽在使用時的方式是 #{myTag name:'Alex' /}
,那么在該標(biāo)簽的模板文件中,就可以用 ${_name}
來引用參數(shù) name
的值。有些標(biāo)簽是支持在使用的時候添加標(biāo)簽體的,如 #{anotherTag} 測試文字 #{/anotherTag}
。對于這種情況,在標(biāo)簽的模板文件中可以用 #{doBody}
來引用標(biāo)簽體中的內(nèi)容。
在介紹完 Play 框架的視圖層之后,下面介紹 HTTP 路由。
回頁首
HTTP 路由
在前面介紹過,Play 框架中的控制器用來接受 HTTP 請求并返回相應(yīng)的響應(yīng)。這個過程的重要一環(huán)就是 HTTP 請求的 URI 與控制器之間的映射關(guān)系。Play 框架提供了靈活的 HTTP 路由功能來完成這個映射。路由信息被保存在 config/routes
文件中,采用簡單的方式進(jìn)行聲明。每條路由記錄包含 3 個元素,分別是 HTTP 方法的名稱、匹配的 URI 模式以及對應(yīng)的控制器動作方法。路由記錄表示的含義是當(dāng)使用給定的 HTTP 方法來請求對應(yīng)模式的 URI 的時候,控制器動作方法就會被調(diào)用。
Play 框架支持的 HTTP 方法有 GET
、POST
、PUT
、DELETE
和 HEAD
。使用通配符 *
可以匹配任何方法。在 URI 模式的聲明中可以使用正則表達(dá)式來表示復(fù)雜的映射規(guī)則。URI 模式中還可以使用 {...}
來聲明動態(tài)的部分。每個動態(tài)部分都是有名稱的,可以在控制器動作方法中通過 params
對象來獲取。比如,/notes/home
這樣的 URI 模式會匹配 /notes/home
,但是 /notes/{id}
可以匹配 /notes/123
和 /notes/abc
,而且 URI 模式中 /notes/
后面的部分可以作為參數(shù) id
的值被獲取到。URI 模式 /notes/{<[0-9]+>id}
使用了正則表達(dá)式,只會匹配 /notes/
后面緊跟的全是數(shù)字的情況。在聲明控制器的動作方法的時候,需要使用帶名稱空間的全名,如 myapp.Notes.show
。有些動作方法是帶參數(shù)的,可以在聲明的時候預(yù)先綁定一些參數(shù)值,這樣可以方便的添加一些 URI 別名。比如動作方法 Notes.show()
有一個參數(shù) id
用來指明要顯示的內(nèi)容的 ID。如果參數(shù) id
的值為 0,則會顯示所有內(nèi)容的一個列表。這樣的話,就可以定義一個類似 GET /notes/all Notes.show(id:0)
的路由聲明。這樣暴露出來的 URI 更加簡潔和易于記憶。
在路由文件中的路由聲明是按照從上到下的優(yōu)先級來進(jìn)行匹配的。比較具體的 URI 模式應(yīng)該放在比較通用的模式之前。對于靜態(tài)文件,可以通過一個特殊的動作方法 staticDir
進(jìn)行聲明。比如 GET /files staticDir:files
就聲明了 files
目錄中包含的是靜態(tài)文件。
在介紹完 HTTP 路由之后,下面介紹 Play 框架獨(dú)特的無狀態(tài)的體系結(jié)構(gòu)。
回頁首
無狀態(tài)的體系結(jié)構(gòu)
HTTP 協(xié)議本身就被設(shè)計成無狀態(tài)的,采用請求 / 響應(yīng)的模式。不同的請求之間并不存在相互關(guān)系。但是這種架構(gòu)模式在開發(fā)某些 Web
應(yīng)用的時候不是很方便。有些應(yīng)用要求用戶進(jìn)行認(rèn)證登錄之后才能進(jìn)行某些操作。同樣的
URL,認(rèn)證和未認(rèn)證用戶看到的內(nèi)容是不同的。而且用戶認(rèn)證成功之后,他應(yīng)該在一段時間內(nèi)保持這種認(rèn)證狀態(tài)。否則的話,用戶每次都需要輸入用戶名和密碼才
能訪問受限的內(nèi)容。對于這種情況,很多 Web 開發(fā)框架提供了會話的支持,允許應(yīng)用保存一些與會話相關(guān)的數(shù)據(jù)。Java Servlet 規(guī)范中的 javax.servlet.http. HttpSession
就是一種會話的接口。應(yīng)用的服務(wù)器會負(fù)責(zé)維護(hù)每個會話相關(guān)的數(shù)據(jù)。這些數(shù)據(jù)可以通過一個會話 ID 來進(jìn)行標(biāo)識。這個標(biāo)識會利用瀏覽器的 cookie
機(jī)制保存在瀏覽器端,也可以作為請求 URL 的參數(shù)來傳遞。服務(wù)器端通過此標(biāo)識來識別每個會話。在處理相應(yīng)的請求的時候,就可以根據(jù)會話 ID
來獲取保存在服務(wù)器端上的會話數(shù)據(jù)。會話機(jī)制的問題是會影響應(yīng)用的可伸縮性。如果一個應(yīng)用使用多臺服務(wù)器的話,就需要額外的機(jī)制來保證同一用戶在不同機(jī)器
上面的會話是同步的。而無狀態(tài)的實現(xiàn)則不存在這個問題,對于某一個請求,由不同機(jī)器來處理的結(jié)果都是相同的。
Play 框架的設(shè)計架構(gòu)就是無狀態(tài)的。它沒有提供服務(wù)器端的機(jī)制用來維護(hù)跨多個請求的數(shù)據(jù)。如果確實需要保存這樣的數(shù)據(jù)的話,可以考慮下面幾種方案:
- 保存在 Session 或 Flash 作用域中。Play
框架中仍然有會話的機(jī)制,但是并沒有提供在服務(wù)器端保存會話數(shù)據(jù)的能力。會話數(shù)據(jù)是保存在瀏覽器的 cookie
中的,由瀏覽器在每次請求的時候自動發(fā)送。通過這種方式來達(dá)到維護(hù)會話數(shù)據(jù)的目的。由于會話數(shù)據(jù)是保存在 cookie
中,其大小是有限制的,一般不能超過 4K 字節(jié),而且只能保存字符串類型的數(shù)據(jù)。Flash 作用域和會話一樣,也是通過 cookie
來保存的。所不同的是,F(xiàn)lash 作用域中的數(shù)據(jù)只在下次請求中是有效的。
- 保存在持久化的數(shù)據(jù)存儲中,如數(shù)據(jù)庫中。如果需要在多個請求中使用同一個領(lǐng)域?qū)ο蟮脑挘梢园堰@個對象的 ID 保存在 Session 或 Flash 作用域中,而在控制器動作方法中使用此 ID 來從數(shù)據(jù)庫中查詢相應(yīng)的對象。
- 保存在暫時性數(shù)據(jù)存儲中,如緩存中。Play 框架內(nèi)置了緩存的支持,通過調(diào)用類
play.cache.Cache
就可以對緩存進(jìn)行操作。與使用持久化存儲類似,緩存中的鍵的值可以保存在 Session 或 Flash 作用域中。
對于熟悉了 Java Servlet 規(guī)范的開發(fā)人員來說,需要一些時間來適應(yīng) Play 框架的這種無狀態(tài)的體系結(jié)構(gòu)。不過這種結(jié)構(gòu)對于應(yīng)用的可伸縮性來說,確實是非常有好處的。
介紹完無狀態(tài)的體系結(jié)構(gòu)之后,下面介紹一些其它話題。
回頁首
其它話題
測試
Play 框架對應(yīng)用的測試也提供了良好的支持。Play 框架一共支持三種類型的測試,分別是單元測試、功能測試和界面測試。單元測試主要用來測試應(yīng)用的模型層代碼。單元測試用例的 Java 類繼承自 play.test.UnitTest
,可以使用 JUnit 4 提供的標(biāo)注和斷言。功能測試主要用來測試應(yīng)用的控制層代碼。功能測試用例的 Java 類繼承自 play.test.FunctionalTest
。在測試用例中,可以通過 GET()
、POST()
、PUT()
、DELETE()
和 makeRequest()
等方法來發(fā)出 HTTP 請求,也可以直接調(diào)用控制器中的動作方法。除此之外,還可以使用一些與 HTTP 響應(yīng)相關(guān)的斷言。如 assertStatus()
、assertContentType()
和 assertHeaderEquals()
分別用來驗證 HTTP 狀態(tài)代碼、內(nèi)容類型和 HTTP 頭。界面測試使用 Selenium 工具來進(jìn)行。開發(fā)人員可以使用 Selenium 的語法來編寫測試用例,也可以使用 Play 框架提供的 #{selenium}
標(biāo)簽。
在進(jìn)行測試的時候,需要準(zhǔn)備一些測試數(shù)據(jù)。測試數(shù)據(jù)可以用 YAML 的格式保存在文本文件中,并通過 play.test.Fixtures.laod()
方法來加載這些數(shù)據(jù)到數(shù)據(jù)庫中。當(dāng)測試結(jié)束之后,可以通過 deleteAll()
方法來刪除這些數(shù)據(jù)。
在測試的時候,需要用 play test
命令以測試模式啟動應(yīng)用,再用瀏覽器訪問 http://localhost:9000/@tests
進(jìn)行測試。
任務(wù)調(diào)度
在 Web 應(yīng)用開發(fā)中,有時候會需要定期執(zhí)行一些調(diào)度任務(wù),比如數(shù)據(jù)庫備份和數(shù)據(jù)同步等。這些任務(wù)不是通過 HTTP 請求來觸發(fā)的,而是定時執(zhí)行的。Play 框架提供了內(nèi)置的任務(wù)調(diào)度支持的能力。創(chuàng)建新任務(wù)的時候,只需要繼承自 play.jobs.Job
類,并覆寫 doJob()
方法即可。如果要創(chuàng)建的任務(wù)有返回結(jié)果的話,覆寫 doJobWithResult()
方法即可。任務(wù)創(chuàng)建完成之后,可以選擇不同的調(diào)度方式。一種方式是在應(yīng)用啟動的時候執(zhí)行一次。只需要在任務(wù)的 Java 類上添加標(biāo)注 @OnApplicationStart
即可。對于定期執(zhí)行的任務(wù),Play 框架提供了兩個標(biāo)注:一個是 @Every
,用來按照固定的時間間隔調(diào)度任務(wù),如 @Every("1h")
聲明任務(wù)每個小時執(zhí)行一次;另外一個是 @On
,用來聲明描述調(diào)度策略的 CRON 表達(dá)式。
安全
Play 框架提供了對 Web 應(yīng)用安全性方面的支持,可以防范一些常見的攻擊方式。前面提到過,Play
框架中的會話數(shù)據(jù)是保存在瀏覽器的 cookie
中的。這些數(shù)據(jù)是經(jīng)過簽名的,可以防止被惡意攻擊者所篡改。應(yīng)用中的重要數(shù)據(jù)也不應(yīng)該保存在會話中。Play 框架中的模板在輸出 HTML
內(nèi)容的時候,會自動對內(nèi)容進(jìn)行轉(zhuǎn)義,可以防范跨站點腳本攻擊。對于 SQL 注入攻擊,開發(fā)人員應(yīng)該盡量使用提供的 find()
方法來查詢領(lǐng)域?qū)ο?。對于自己?chuàng)建的查詢語句,應(yīng)該在語句中使用占位符并進(jìn)行參數(shù)綁定,而不是通過字符串相加的方式來創(chuàng)建。為了防范跨站點請求偽造,Play 框架中的控制器的動作方法都可以使用 checkAuthenticity()
方法來聲明調(diào)用此方法時的請求中必須包含合法的令牌。這個令牌用來確保當(dāng)前請求是由應(yīng)用自身發(fā)出的,而不是被偽造的。通過 session.getAuthenticityToken()
方法可以生成一個只對當(dāng)前會話有效的令牌,需要在請求的時候附帶此令牌。如果是通過頁面上的表單來提交請求的話,Play 框架也提供了一個標(biāo)簽 #{authenticityToken /}
用來生成一個包含了令牌的隱藏域,可以直接在模板中使用。
回頁首
總結(jié)
Play 框架作為一個優(yōu)秀的 Java Web 應(yīng)用開發(fā)框架,可以幫助開發(fā)人員快速高效的構(gòu)建 Web
應(yīng)用。它為開發(fā)人員提供了一個良好的基礎(chǔ)架構(gòu),并屏蔽了很多底層的實現(xiàn)細(xì)節(jié)。開發(fā)人員可以用一個簡單的視角來看待 Web
應(yīng)用開發(fā),而不需要關(guān)心過多的細(xì)節(jié)。Web 開發(fā)人員可以熟悉 Play 框架,并在開發(fā)中選用這個框架。