1 高級HTTP綁定#
簡單類型
Play可以實現所有Java原生的簡單數據類型的自動轉換,主要包括:int,long,boolean,char,byte,float,double,Integer,Long,Boolean,Char,String,Float,Double。
日期類型
如果HTTP參數字符串符合以下幾種數據格式,框架能夠自動將其轉換爲日期類型:
- yyyy-MM-dd'T'hh:mm:ss’Z' // ISO8601 + timezone
- yyyy-MM-dd'T'hh:mm:ss" // ISO8601
- yyyy-MM-dd
- yyyyMMdd'T'hhmmss
- yyyyMMddhhmmss
- dd'/'MM'/'yyyy
- dd-MM-yyyy
- ddMMyyyy
- MMddyy
- MM-dd-yy
- MM'/'dd'/'yy
而且還能通過@As註解,指定特定格式的日期,例如:
archives?from=21/12/1980
public static void articlesSince(@As("dd/MM/yyyy") Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
也可以根據不同地區的語言習慣對日期的格式做進一步的優化,具體如下:
public static void articlesSince(@As(lang={"fr,de","*"},
value={"dd-MM-yyyy","MM-dd-yyyy"}) Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
在這個例子中,對於法語和德語的日期格式是dd-MM-yyyy,其他語言的日期格式是MM-dd-yyyy。語言值可以通過逗號隔開,且需要與參數的個數相匹配。
如果沒有使用@As註解來指定,Play會採用框架默認的日期格式。爲了使默認的日期格式能夠正常工作,按照以下方式編輯application.conf文件:
date.format=yyyy-MM-dd
在application.conf文件中設置默認的日期格式之後,就可以通過${date.format()}方法對模板中的日期進行格式化操作了。
日曆類型
日曆類型和日期類型非常相像,當然Play會根據本地化選擇默認的日曆類型。讀者也可以通過@Bind註解來使用自定義的日曆類型。
文件類型
在Play中處理文件上傳是件非常容易的事情,首先通過multipart/form-data編碼的請求將文件發送到服務器,然後使用java.io.File類型提取文件對象:
public static void create(String comment, File attachment) {
String s3Key = S3.post(attachment);
Document doc = new Document(comment, s3Key);
doc.save();
show(doc.id);
}
新創建文件的名稱與原始文件一致,保存在應用的臨時文件下(Application_name/tmp)。在實際開發中,需要將其拷貝到安全的目錄,否則在請求結束後會丟失。
數組和集合類型
所有Java支持的數據類型都可以通過數組或者集合的形式來獲取。數組形式:
public static void show(Long[] id){List形式:
...
}
public staic void show(List<Long> id){集合形式:
...
}
public static void show(Set<Long> id){
...
}
public static void show(Map<String, String> client) {
...
}
?user.name=John&user.phone=111-1111&user.phone=222-2222
POJO對象綁定
Play使用同名約束規則(即HTTP參數名必須與模型類中的屬性名一致),自動綁定模型類:
public static void create(Client client){
client.save();
show(client);
}
以下的查詢字符串可以通過上例的Action創建client:
?client.name=Zenexity&client.email=contact@zenexity.fr
參數綁定是遞歸執行的,這意味着可以深入到關聯對象:
?client.name=Zenexity
&client.address.street=64+rue+taitbout
&client.address.zip=75009
&client.address.country=France
Play的參數綁定提供數組的支持,可以將對象id作爲映射規則,更新一組模型對象。假設Client模型有一組聲明爲List<Customer>的customers屬性,那麼更新該屬性需要使用如下查詢字符串:
?client.customers[0].id=123
&client.customers[1].id=456
&client.customers[2].id=789
2 JPA對象綁定#
通過HTTP參數還可以實現JPA對象的自動綁定。Play會識別HTTP請求中提供的參數user.id,自動與數據庫中User實例的id進行匹配。一旦匹配成功,HTTP請求中的其他User屬性參數可以直接更新到數據庫相應的User記錄中:
public static void save(User user){
user.save();
}
和POJO映射類似,可以使用JPA綁定來更改對象,但需要注意的是必須爲每個需要更改的對象提供id:
user.id = 1
&user.name=morten
&user.address.id=34
&user.address.street=MyStreet
3 自定義綁定#
綁定機制支持自定義功能,可以按照讀者的需求,自定義參數綁定的規則。
@play.data.binding.As
@play.data.binding.As註解可以依據配置提供綁定的支持。下例使用DateBinder指定日期的數據格式:
public static void update(@As("dd/MM/yyyy") Date updatedAt) {
...
}
@As註解還具有國際化支持,可以爲每個本地化提供專門的註解:
public static void update(
@As(
lang={"fr,de","en","*"},
value={"dd/MM/yyyy","dd-MM-yyyy","MM-dd-yy"}
)
Date updatedAt
) {
...
}
@As註解可以和所有支持它的綁定一起工作,包括用戶自定義的綁定。以下是使用ListBinder的例子:
public static void update(@As(",") List<String> items) {
...
}
上例中的綁定使用逗號將字符串分隔成List。
@play.data.binding.NoBinding
@play.data.binding.NoBinding註解允許對不需要綁定的屬性進行標記,以此來解決潛在的安全問題。比如:
//User爲Model類
public class User extends Model {
@NoBinding("profile") public boolean isAdmin;
@As("dd, MM yyyy") Date birthDate;
public String name;
}
//editProfile爲Action方法
public static void editProfile(@As("profile") User user) {
...
}
在上述例子中,user對象的isAdmin屬性始終不會被editProfile方法(Action)所修改,即使有惡意用戶僞造POST表單提交user.isAdmin=true信息,也不能修改user的isAdmin權限。
play.data.binding.TypeBinder
@As註解還提供完全自定義綁定的功能。自定義綁定必須是TypeBinder類的實現:
public class MyCustomStringBinder implements TypeBinder<String> {
public Object bind(String name, Annotation[] anns, String value,
Class clazz) {
return "!!" + value + "!!";
}
}
public static void anyAction(@As(binder=MyCustomStringBinder.class) String name) {
...
}
@play.data.binding.Global
Play中還可以自定義全局Global綁定。以下是爲java.awt.Point類定義綁定的例子:
@Global
public class PointBinder implements TypeBinder<Point> {
public Object bind(String name, Annotation[] anns, String value,
Class class) {
String[] values = value.split(",");
return new Point(
Integer.parseInt(values[0]),
Integer.parseInt(values[1])
);
}
}
因此外部模塊很容易通過自定義綁定來提供可重用的類型轉換組件。
3、結果返回
Action方法需要對客戶端作出HTTP響應,最簡單的方法就是發送結果對象。當對象發送後,常規的執行流程就會中斷。以下面這段代碼爲例,最後一句System.out.println的輸出不會被執行:
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
System.out.println("This message will never be displayed !");
}
render(…)方法向模板發送client對象,之後的其他語句將不會執行,所以在控制檯中,並不會打印出“This message will never be displayed !”。
3.1 返回文本內容#
renderText(…)方法直接將文本內容寫到底層HTTP響應中:
public static void countUnreadMessages(){
Integer unreadMessages=MessagesBos.countUnreadMessage();
renderText(unreadMessages);
}
也可以通過Java標準的格式化語法對輸出的文本進行處理:
public static void countUnreadMessages(){
Integer unreadMessages=MessagesBox.countUnreadMessages();
renderText("There are %s unread messages",unreadMessages);
}
3.2 返回JSON字符串#
越來越多的應用使用JSON作爲數據格式進行交互,Play對此進行了很好的封裝,只需要使用renderJSON(…)方法就可以輕鬆地返回JSON字符串。在使用renderJSON(…)方法時,Play會自動將服務器返回的響應的content type值設置爲application/json,並且將renderJSON(...)方法中的參數以JSON格式返回。
在使用renderJSON(...)方法時,可以輸入字符串格式的參數,自行指定JSON返回的內容。
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderJSON("{\"messages\": " + unreadMessages +"}");
}
以上範例在使用renderJSON(...)方法時,傳入了拼接成JSON格式的字符串參數。Play框架會對其進行自動設置,改變content type的值爲application/json。
當然,renderJSON(...)方法的功能並不只有這些。因爲大部分的應用需求,都會要求服務端返回比較複雜的JSON格式,如果都採用字符串拼接的方式組成JSON內容,就太不人性化了。renderJSON(...)的輸入參數還可以是複雜的對象,如果採用這種方式使用renderJSON(...)方法,Play在執行renderJSON(...)時,底層會先調用GsonBuilder將對象參數進行序列化,之後再將複雜的對象以JSON的格式返回給請求。這樣開發者就可以完全透明地使用renderJSON(...)方法,不需要做其他的任何操作了,以下代碼範例將會展示renderJSON(...)的這個功能。
public static void getUnreadMessages() {
List<Message> unreadMessages = MessagesBox.unreadMessages();
renderJSON(unreadMessages);
}
3.3 返回XML字符串#
與使用renderJSON(...)方法返回JSON內容類似,如果用戶希望以XML格式對內容進行渲染,可以在Controller控制器中直接使用renderXml(…)方法。 使用renderXml(...)方法時,Play會自動將服務器返回的響應的content type值設置爲application/xml。
在使用renderXml(...)方法時,可以輸入字符串格式的參數,自行指定XML返回的內容。
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderXml("<unreadmessages>"+unreadMessages+"</unreadmessages>");
}
如果希望將複雜的對象以XML格式進行渲染,可以在使用renderXml(...)方法時輸入org.w3c.dom.Document格式的對象,或者直接輸入POJO對象。以POJO對象作爲參數使用renderXml(...)方法時,Play會使用XStream將其進行序列化操作。同樣的,這些序列化操作都不需要由開發者去做,全部交給Play就行,開發者需要做的就是按照規範簡單地調用renderXml(...)方法即可。
public static void getUnreadMessages() {
Document unreadMessages = MessagesBox.unreadMessagesXML();
renderXml(unreadMessages);
3.4 返回二進制內容#
Play爲開發者提供了renderBinary(...)方法,可以非常方便的返回二進制數據(如存儲在服務器裏的文件、圖片等)給客戶端。以下代碼範例將會展示如何使用renderBinary(...)方法進行二進制圖片的渲染。
public static void userPhoto(long id) {
final User user = User.findById(id);
response.setContentTypeIfNotSet(user.photo.type());
java.io.InputStream binaryData = user.photo.get();
renderBinary(binaryData);
}
首先,開發者需要建立用於持久化的域模型User,該User模型具有play.db.jpa.Blob類型的屬性photo。play.db.jpa.Blob是經過Play封裝的特有的屬性類型,可以很方便的處理二進制數據。之後,在Controller控制器中使用時,需要調用域模型的findById(...)方法加載持久化的數據,並將圖片以二進制數據流InputStream的形式進行渲染。
3.5 下載附件功能#
如果開發者希望將存儲在服務器端的文件,採用下載的形式渲染給客戶端用戶,需要對HTTP的header進行設置。通常的做法是通知Web瀏覽器將二進制響應數據以附件的形式,下載至用戶的本地電腦上。在Play中完成這個功能非常簡單,只需要在使用renderBinary(...)方法時多傳入一個文件名的參數即可。這樣做會觸發renderBinary(...)的額外功能,提供文件名並設置響應頭的Content-Disposition屬性。之後二進制文件(包括圖片)將會以附件下載的形式,渲染給用戶。
public static void userPhoto(long id) {
final User user = User.findById(id);
response.setContentTypeIfNotSet(user.photo.type());
java.io.InputStream binaryData = user.photo.get();
renderBinary(binaryData, user.photoFileName);
}
3.6 執行模板#
如果需要響應的內容比較複雜,那麼就應該使用模板來進行處理:
public class Clients extends Controller{
public static void index(){
render();
}
}
模板的名稱遵從Play的約束規則,默認的模板路徑採用控制器和Action的名稱相結合的方式來定義,比如在上述例子中,模板對應的路徑爲:app/views/Clients/index.html。
3.7 爲模板作用域添加數據#
通常情況下模板文件都需要數據進行顯示,可以使用renderArg()方法爲模板注入數據:
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
renderArgs.put("client", client);
render();
}
}
在模板執行過程當中,client變量可以被使用:
<h1>Client ${client.name}</h1>
3.8 更簡單方式#
這裏介紹一種更簡單的方式向模板傳遞數據。直接使用render(…)方法注入模板數據:
public static void show(Long id){
Client client=Client.findById(id);
render(client);
}
以該方式進行數據傳遞,模板中可訪問的變量與Java本地變量的名稱(也就是render()方法中的參數名)一致。當然也可以同時傳遞多個參數:
public static void show(Long id){
Client client=Client.findById(id);
render(id,client);
}
render()方法只允許傳遞本地變量。
3.9 指定其他模板進行渲染#
如果讀者不希望使用默認的模板進行渲染,那麼可以在renderTemplate(…)方法的第一個參數中指定其他自定義的模板路徑,例如:
public static void show(Long id) {
Client client = Client.findById(id);
renderTemplate("Clients/showClient.html", id, client);
3.10 重定向URL#
redirect(…)方法產生HTTP重定向響應,可以將請求轉發到其他URL:
public static void index(){
redirect("http://www.oopsplay.org");
}
3.11 自定義Web編碼#
Play推薦開發者使用UTF-8作爲應用開發的編碼格式,如果不進行任何設置,Play框架默認使用的也就是UTF-8格式。但是具體情況並不總是這麼理想,有些特殊的需求可能要求某些響應(response)的格式爲ISO-8859-1,或者要求整個應用都必須保持ISO-8859-1編碼。
爲當前響應設置編碼格式
如果需要改變某一個響應(response)的編碼格式,可以直接在Controller控制器中進行修改,具體做法如下所示:
response.encoding = "ISO-8859-1";
<form action="@{application.index}" method="POST" accept-charset="ISO-8859-1">
<input type="hidden" name="_charset_" value="ISO-8859-1">
</form>
定義全局編碼格式
通常情況下,整個應用應該保持統一的編碼格式。如果開發者需要設置應用全局的編碼格式,可以在application.conf配置文件中修改application.web_encoding屬性,配置相應的編碼。
4、Action鏈
lay中的Action鏈與Servlet API中的forward不盡相同。Play的每次HTTP請求只能調用一個Action,如果需要調用其他的Action,那麼必須將瀏覽器重定向到相應的URL。在這種情況下,瀏覽器的URL始終與正在執行的Action保持對應關係,使得後退、前進、刷新操作更加清晰。
調用控制器中其他Action方法也可以實現重定向,框架會攔截該調用並生成正確的HTTP重定向。具體實現如下:
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
public static void create(String name) {
Client client = new Client(name);
client.save();
show(client.id);
}
}
相應的路由規則定義如下:
GET /clients/{id} Clients.show
POST /clients Clients.create
按照定義,Action鏈的生命週期爲:
- 瀏覽器向/clients發送POST請求;
- 路由器調用Clients控制器中的create方法;
- create方法直接訪問show方法;
- Java調用被攔截,路由器逆向生成帶有id參數的URL來調用Clients.show;
- HTTP響應重定向爲:/clients/3132;
- 瀏覽器地址欄中URL展現爲:/clients/3132;