1、集成OpenID
OpenID是身份識別系統,具有開放,非集中等特點。我們只需要記錄OpenID授權用戶的使用信息,不必保持用戶的特定狀態,就可以在程序中很容易地識別新用戶。
OpenID是去中心化的網上身份認證系統。對於支持OpenID的網站,用戶不需要記住像用戶名和密碼這樣的傳統驗證標記。取而代之的是,他們只需要預先在一個作爲OpenID身份提供者(identity provider, IdP)的網站上註冊。OpenID是去中心化的,任何網站都可以使用OpenID來作爲用戶登錄的一種方式,任何網站也都可以作爲OpenID身份提供者。OpenID既解決了問題而又不需要依賴於中心性的網站來確認數字身份。
下面實例將演示在Play應用中如何使用OpenID認證,以下是OpenID認證過程:
- 針對每個用戶請求,先檢查用戶是否已經連接OpenID。
- 如果用戶沒有連接OpenID,跳轉至用戶可以提交OpenID的頁面。
- 將用戶提交的OpenID重定向到OpenID提供商。
- 返回後,得到驗證通過的OpenID用戶信息,並將它保存在HTTP Session中。
Play中的OpenID功能由play.libs.OpenID輔助類提供,基於OpenID4Java實現。我們在控制器中定義authenticate()方法,處理用戶提交的OpenID。該方法先進行用戶OpenID的檢查,如果已經連接,則將用戶信息保存在HTTP Session中,並顯示歡迎頁面。如果用戶是第一次提交OpenID,則將提交的OpenID重定向到OpenID提供商:
@Before(unless={"login", "authenticate"})
static void checkAuthenticated() {
if(!session.contains("user")) {
login();
}
}
public static void index() {
render("Hello %s!", session.get("user"));
}
public static void login() {
render();
}
public static void authenticate(String user) {
if(OpenID.isAuthenticationResponse()) {
UserInfo verifiedUser = OpenID.getVerifiedID();
if(verifiedUser == null) {
flash.error("Oops. Authentication has failed");
login();
}
session.put("user", verifiedUser.id);
index();
} else {
if(!OpenID.id(user).verify()) { // will redirect the user
flash.error("Cannot verify your OpenID");
login();
}
}
}
創建Login.html模板,添加用戶提交OpenID的表單:
#{if flash.error}
<h1>${flash.error}</h1>
#{/if}
<form action="@{Application.authenticate()}" method="POST">
<label for="user">What’s your OpenID?</label>
<input type="text" name="user" id="user" />
<input type="submit" value="login..." />
</form>
最後定義路由:
GET / Application.index
GET /login Application.login
* /authenticate Application.authenticate
2、Ajax請求
2.1 集成jQuery#
Play默認集成了jQuery庫,並將其存放於應用的public/javascripts目錄。框架對jQuery進行了封裝,提供#{jsAction /}標籤簡化了請求異步調用控制器中的Action方法,因此我們可以通過jQuery非常方便地處理Ajax請求。本節將以簡單的Ajax應用爲例,介紹在Play框架中如何使用jQuery提供的Ajax支持。
2.2 使用#{jsAction /}#
Play提供的#{jsAction /}標籤會返回一個JavaScript函數,該JavaScript函數由基於Action方法的URL連接和自由變量組成。它並不會自動執行Ajax請求,而需要我們先手動對返回的URL進行配置。
下面介紹Play應用的Ajax實例。首先在控制器Hotels中定義Action方法list,用於處理瀏覽器異步提交的請求。list方法定義完成後爲其配製路由規則:
GET /hotels/list Hotels.list
我們在客戶端中可以通過以下方式導入路由:
<script type="text/javascript">
var listAction = #{jsAction @list(':search', ':size', ':page') /}
$('#result').load(
listAction({search: 'x', size: '10', page: '1'}),
function() {
$('#content').css('visibility', 'visible')
}
)
</script>
在這個例子中,我們向Hotels控制器中的list方法發送請求,請求中包含search,size和page三個參數。之後將請求保存在listAction變量中,使用load函數通過jQuery處理該請求(這裏處理的是HTTP GET請求)。以下就是所發送請求的具體URL:
GET /hotels/list?search=x&size=10&page=1
以這種方式發送請求會返回HTML數據。當然,我們也可以在控制器中使用適當的渲染方法,返回其他格式的數據,比如renderJSON, renderXML或者直接使用XML的模版等。在客戶端接收到JSON或者XML數據後,可以通過jQuery進行格式轉換。如果讀者還想了解更多細節問題,可以查閱jQuery相關內容。
如果讀者需要使用POST請求,只需要將jQuery方法進行轉換即可:
$.post(listAction(), function(data) {
$('#result').html(data);
});
2.3 使用#{jsRoute /}#
Play提供的#{jsRoute /}標籤,可以幫助開發者更好地管理路由。#{jsRoute /}標籤的使用方法很簡單,並且與#{jsAction /}類似,但是不同的地方爲#{jsRoute /}標籤返回的是一個對象。該對象包含基於服務端Action的URL,以及相應的HTTP方法(GET、POST等),具體範例如下所示。
<script type="text/javascript">
var updateUserRoute = #{jsRoute @Users.update(':id') /}
$.ajax({
url: updateUserRoute.url({id: userId}),
type: updateUserRoute.method,
data: 'user.name=Guillaume'
});
</script>
使用#{jsRoute /}標籤所帶來的好處是顯而易見的,開發者只需要修改routes路由文件,就可以統一地改變HTTP方法,而不再需要一個一個查看和修改模板文件了。
3、管理數據庫升級
開發人員使用關係數據庫時,通常需要跟蹤和管理數據庫結構的升級和變化。當出現以下幾種情況時,我們需要使用更成熟的方式跟蹤和管理數據庫結構的變化:
- 在團隊開發中,每個成員都需要知道數據庫結構的任何變化。
- 當成品部署到服務器上後,需要採用安全穩定的方式去升級數據庫結構。
- 當開發人員在不同的機器上工作時,需要保持數據庫同步。
如果採用JPA進行操作,Hibernate會自動處理數據庫結構的升級。如果讀者經常需要手動管理數據庫結構,並進行一些精細的調整,版本控制就變得必不可少了。
3.1 升級腳本#
Play通過編寫升級腳本來跟蹤數據庫結構的變化。這些腳本採用標準的SQL語句作爲語法規則,存放在應用程序的db/evolutions目錄下。腳本使用統一的命名規則:編寫的第一個腳本命名爲1.sql,第二個腳本爲2.sql,並以此類推。每個腳本都包含兩個部分:
- Ups部分用於描述需要改變的地方。
- Downs部分用於描述如何還原。
以下是數據庫升級腳本的例子,起到引導作用:
# Users schema
# --- !Ups
CREATE TABLE User (
id bigint(20) NOT NULL AUTO_INCREMENT,
email varchar(255) NOT NULL,
password varchar(255) NOT NULL,
fullname varchar(255) NOT NULL,
isAdmin boolean NOT NULL,
PRIMARY KEY (id)
);
# --- !Downs
DROP TABLE User;
在編寫數據庫升級腳本時,須使用註釋明確地將Ups和Downs部分區分開。
如果讀者已經在application.conf文件中配置了數據庫,並且編寫了數據庫升級腳本(放置在db/evolutions目錄下),應用在啓動時就會自動激活數據庫的升級模式。我們也可以強制將其關閉,只需在application.conf文件中進行如下配置:
evolutions.enabled=false
(圖1 數據庫結構不同步)
在頁面上點擊Apply evolutions按鈕,就可以直接執行SQL升級腳本。
如果應用使用內存數據庫(db=mem),Play會預先對數據庫進行檢測。如果數據庫爲空就會自動執行所有的升級腳本。
3.2 同步#
協作開發中,保持應用同步非常重要。設想一下,如果有兩個開發者合力開發項目,開發者A因爲自己負責的功能模塊需要,創建了新的數據表並編寫了升級腳本2.sql:
# Add Post
# --- !Ups
CREATE TABLE Post (
id bigint(20) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content text NOT NULL,
postedAt date NOT NULL,
author_id bigint(20) NOT NULL,
FOREIGN KEY (author_id) REFERENCES User(id),
PRIMARY KEY (id)
);
# --- !Downs
DROP TABLE Post;
Play會將該升級腳本應用於開發者A的數據庫。與此同時,開發者B因功能需求修改了User表,也編寫了升級腳本,同樣命名爲2.sql:
# Update User
# --- !Ups
ALTER TABLE User ADD age INT;
# --- !Downs
ALTER TABLE User DROP age;
開發者B先完成了負責的功能模塊,並提交了代碼(比如開發者採用Git進行管理)。開發者A需要先整合兩個人前半段的工作結果,才能開展後續工作,但是在執行git pull的時候,合併會出現如下衝突:
Auto-merging db/evolutions/2.sql
CONFLICT (add/add): Merge conflict in db/evolutions/2.sql
Automatic merge failed; fix conflicts and then commit the result.
這是因爲開發者A和開發者B都創建了2.sql升級腳本,必須進行整合:
<<<<<<< HEAD
# Add Post
# --- !Ups
CREATE TABLE Post (
id bigint(20) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content text NOT NULL,
postedAt date NOT NULL,
author_id bigint(20) NOT NULL,
FOREIGN KEY (author_id) REFERENCES User(id),
PRIMARY KEY (id)
);
# --- !Downs
DROP TABLE Post;
=======
# Update User
# --- !Ups
ALTER TABLE User ADD age INT;
# --- !Downs
ALTER TABLE User DROP age;
>>>>>>> devB
整合工作非常簡單,只需要將衝突的部分合並即可:
# Add Post and update User
# --- !Ups
ALTER TABLE User ADD age INT;
CREATE TABLE Post (
id bigint(20) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content text NOT NULL,
postedAt date NOT NULL,
author_id bigint(20) NOT NULL,
FOREIGN KEY (author_id) REFERENCES User(id),
PRIMARY KEY (id)
);
# --- !Downs
ALTER TABLE User DROP age;
DROP TABLE Post;
將整合後的腳本命名爲2.sql,作爲升級腳本的最新修訂版(注意:與前期開發者A應用到數據庫的2.sql不同)。Play會發現這個情況,並詢問開發者A是否同步數據庫,將最新的修訂版2.sql替換原來的版本,如圖2所示。
(圖2 同步升級腳本)
3.3 非同步狀態#
如果腳本(比如3.sql)中存在錯誤,數據庫升級工作將無法完成。當出現這種情況時,Play會將數據庫的結構標記爲非同步狀態,並要求我們手動解決這個問題,之後才能繼續別的操作。下例的Ups部分存在錯誤:
# Add another column to User
# --- !Ups
ALTER TABLE Userxxx ADD company varchar(255);
# --- !Downs
ALTER TABLE User DROP company;
該腳本在執行時會出現錯誤,Play將數據庫的結構標記爲非同步狀態,如圖3所示:
(圖3非同步狀態)
錯誤必須得到修正後,才能進行後續操作。使用SQL命令手動更改數據庫結構:
ALTER TABLE User ADD company varchar(255);
然後點擊Mark it resolved按鈕,通知框架問題已經解決。由於原先的升級腳本存在錯誤,必須將其修正。修改3.sql腳本文件:
# Add another column to User
# --- !Ups
ALTER TABLE User ADD company varchar(255);
# --- !Downs
ALTER TABLE User DROP company;
Play會發現該升級腳本是最新的,因此覆蓋原有的3.sql執行,如圖4所示:
(圖4 成功執行數據庫結構同步)
圖4 所顯示的是腳本正確執行的結果,之後可以繼續別的工作。
介紹一個開發小技巧:開發人員可以在每次開發前手動刪除原有的數據庫,當應用啓動時框架就會自動加載所有的升級腳本。這樣做的好處是可以使開發人員的數據庫一直保持最新版本。
3.4 升級命令#
當應用處於DEV模式時,數據庫升級是交互式地進行的。但是在PROD模式下,必須先使用play evolutions命令將數據庫結構升級爲最新版本,纔可以開啓應用。當框架發現應用程序的數據庫不是最新版本時,控制檯會出現如下錯誤信息:
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! master-localbuild, http://www.playframework.org
~ framework ID is prod
~
~ Ctrl+C to stop
~
13:33:22 INFO ~ Starting ~/test
13:33:22 INFO ~ Precompiling ...
13:33:24 INFO ~ Connected to jdbc:mysql://localhost
13:33:24 WARN ~
13:33:24 WARN ~ Your database is not up to date.
13:33:24 WARN ~ Use `play evolutions` command to manage database evolutions.
13:33:24 ERROR ~
@662c6n234
Can't start in PROD mode with errors
Your database needs evolution!
An SQL script will be run on your database.
play.db.Evolutions$InvalidDatabaseRevision
at play.db.Evolutions.checkEvolutionsState(Evolutions.java:323)
at play.db.Evolutions.onApplicationStart(Evolutions.java:197)
at play.Play.start(Play.java:452)
at play.Play.init(Play.java:298)
at play.server.Server.main(Server.java:141)
Exception in thread "main" play.db.Evolutions$InvalidDatabaseRevision
at play.db.Evolutions.checkEvolutionsState(Evolutions.java:323)
at play.db.Evolutions.onApplicationStart(Evolutions.java:197)
at play.Play.start(Play.java:452)
at play.Play.init(Play.java:298)
at play.server.Server.main(Server.java:141)
該錯誤信息提示我們運行play evolutions命令同步數據庫:
$ play evolutions
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! master-localbuild, http://www.playframework.org
~ framework ID is gbo
~
~ Connected to jdbc:mysql://localhost
~ Application revision is 3 [15ed3f5] and Database revision is 0 [da39a3e]
~
~ Your database needs evolutions!
# ----------------------------------------------------------------------------
# --- Rev:1,Ups - 6b21167
CREATE TABLE User (
id bigint(20) NOT NULL AUTO_INCREMENT,
email varchar(255) NOT NULL,
password varchar(255) NOT NULL,
fullname varchar(255) NOT NULL,
isAdmin boolean NOT NULL,
PRIMARY KEY (id)
);
# --- Rev:2,Ups - 9cf7e12
ALTER TABLE User ADD age INT;
CREATE TABLE Post (
id bigint(20) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content text NOT NULL,
postedAt date NOT NULL,
author_id bigint(20) NOT NULL,
FOREIGN KEY (author_id) REFERENCES User(id),
PRIMARY KEY (id)
);
# --- Rev:3,Ups - 15ed3f5
ALTER TABLE User ADD company varchar(255);
# ----------------------------------------------------------------------------
~ Run `play evolutions:apply` to automatically apply this script to the db
~ or apply it yourself and mark it done using `play evolutions:markApplied`
~
使用play evolutions:apply命令可以通知Play自動執行數據庫升級腳本:
play evolutions:apply
如果在成品數據庫上手動執行腳本,可以使用play evolutions:markApplied命令通知Play該數據庫已經是最新版本了:
play evolutions:markApplied
當應用處於DEV模式,如果框架自動執行升級腳本失敗,就需要手動解決,並將數據庫結構標記爲已修正的:
play evolutions:resolve