2009年4月6日 星期一

[Struts2] 建立自訂的 Interceptor

我們已經介紹了 Struts2 framework 中很重要的 component 之一的 Interceptor 的重要特性,雖然 Struts2 framework 中已經有很多內建的 interceptors 幫助我們減少很多工作,但是有時候我們還是有自己的特殊需求,所以我們就必須自己撰寫我們的 interceptor。
根據我們之前在深入探討 Interceptor 中提過的,一個 interceptor 中的生命週期有三步驟:1) Preprocessing、2) ActionInvocation.invoke() 與 3) Postprocessing。現在我們就遵循這三個步驟來撰寫我們自己的 interceptor。
以下的範例目的在於判斷使用者是否有登入本系統,如果沒有登入或是 session 中沒有使用者登入的紀錄就將頁面回到首頁。我們將整個 web application 規劃如下:

首先是 interceptor package 中的 MyInterceptor,這個 class 就是我們的主角,這個 interceptor 就是要負責檢查使用者是否有登入系統。
另外我規劃了一個 interface,就是 aware package 中的 PrivateAware interface,這個 interface 只是一個標記用的 interface,用來標記某個 action 是否需要經過 interceptor 的檢查。
在 bean package 中的 User class 就如同之前的範例一樣,用來儲存使用者的資料,因為目前不是 focus 在資料儲存,所以不會將這些資料儲存在資料庫中。
在 login package 中的兩個 class:LoginAction 與 LogoutAction 主要是用來執行登入與登出的 action。這兩個 actions 負責對 session 進行存取,並更新使用者是否有登入的狀態。
最後就是 info package 中的 UserInformationAction class,主要是負責顯示使用者登入後的資訊,這也是我們用來測試如果使用者沒有登入系統而想要執行 UserInformation action,系統會自動將網頁轉跳至登入的頁面。

接下來我們就將這幾個部份深入探討。
首先我們先將我們整個範例所需要的程式先探討,接著才會探討我們今天的主角 MyInterceptor。一開始就是用來標記 action 是否需要進行檢查使用者登入狀態的 interface:
public interface PrivateAware
{
public static final String USER_SESSION = "user";
}

這個 interface 很簡單,沒有任何 method 需要實做,這就是為什麼他被稱為標記用的 interface。另外我們在 PrivateAware interface 中另外宣告了一個 constant USER_SESSION,這是用來儲存使用者資訊除存在 session 的字串。

接著就是登入跟登出的 action,我們的登入登出 actions 都會將使用者資訊儲存到 session 中,所以這兩個 actions 都會 implements SessionAware,這兩個 actions 的內容都很簡單我們就不贅述了:
public class LoginAction extends ActionSupport implements SessionAware
{
private User user;
private Map<String,Object> session;

public User getUser()
{
return user;
}

public void setUser(User user)
{
this.user = user;
}

@Override
public String execute() throws Exception
{
String result = SUCCESS;
if (this.user.getName().equals("Silver") && this.user.getPassword().equals("hi"))
{
this.session.put(PrivateAware.USER_SESSION, this.user);
}
else
{
this.session.remove(PrivateAware.USER_SESSION);
result = LOGIN;
}
return result;
}

@Override
public void setSession(Map session)
{
this.session = session;
}

}
public class LogoutAction extends ActionSupport implements SessionAware
{
private Map<String, Object> session;

@Override
public String execute() throws Exception
{
this.session.remove(PrivateAware.USER_SESSION);
return LOGIN;
}

@Override
public void setSession(Map session)
{
this.session = session;
}

}

接下來就是我們的重頭戲了!我們自己設計的 interceptor 都需要 implements Interceptor interface,並且要 implements intercept() method!以下就是我們的 MyInterceptor:
public class MyInterceptor implements Interceptor
{
@Override
public void destroy()
{
}
@Override
public void init()
{
}
@SuppressWarnings("unchecked")
@Override
public String intercept(ActionInvocation actionInvocation) throws Exception
{
String result = null;
//Proprocessing
if (actionInvocation.getAction() instanceof PrivateAware)
{
Map<String, Object> session = actionInvocation.getInvocationContext().getSession();
User user = (User) session.get(PrivateAware.USER_SESSION);
if (user == null)
{
return Action.LOGIN;
}
}
//ActionInvocation.invoke()
result = actionInvocation.invoke();
//Postprocessing
return result;
}

}

我們在我們的 preprocessing 階段就檢查現在使用者所呼叫的 action 是否有 implements PrivateAware,如果有我們才會進行檢查的作業。接下來我們就會檢查 session 有沒有使用者登入過得狀態,也就是查詢 PrivateAware.USER_SESSION 字串。如果沒有就回傳 LOGIN 的控制字串給 Struts2 framework,而這個控制字串是定義在 struts.xml 中的 global-result,也就是整個在使設定檔之下的 components 都可以共用的!接這就是呼叫 actionInvocation.invoke() method,利用 recursive 的方式來走訪整個 interceptor stack,當我們在 postprocessing 時,我們已經無法更改使用者接下來會看到的頁面了!因為在呼叫 invoke() method 之後,使用者的下一個頁面就已經被決定了!所以我們的程式要在 preprocessing 中執行判斷使用者登入狀況,而不能在 postprocessing 中判斷!

撰寫好 interceptor 之後,我們就要在設定檔中設定我們的 interceptor:
<package name="default" extends="struts-default">
<interceptors>
<interceptor name="authInterceptor" class="silver8250.interceptor.MyInterceptor" />
<interceptor-stack name="authorizationStack">
<interceptor-ref name="authInterceptor" />
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="authorizationStack" />
<global-results>
<result name="login">/index.jsp</result>
</global-results>
<!-- 設定 actions -->
</package>

首先我們要先透過 interceptors tag 來宣告我們自己的 interceptor stack,接著我們就將我們的 MyInterceptor 宣告,並且設定我們的 interceptor-stack tag,因為我們的 package 是繼承 struts-default,所以我們在 interceptor-stack 中可以 reuse defaultStack。最後我們就設定我們的 default-interceptor-ref tag,將預設的 interceptor stack 更改為我們剛剛設定的 interceptor stack!

最後就是設定 global-result tag,當 interceptor 檢查到使用者沒有登入的話,就將頁面轉回到 index.jsp。

而我們的 UserInformationAction class 就是一個 action 並且 implements PrivateAware interface,來告知 MyInterceptor 要檢查此 action 在執行前使用者是否有登入!

在這裡我們簡單的介紹了如何撰寫我們自己的 interceptor,其實要撰寫這樣的 component 不難,也因為 interceptor 採用了 AOP(aspect-oriented programming) 的精神,讓我們撰寫的 interceptor 可以在 action 的生命週期中被 reuse!

4 則留言:

didifong 提到...

阿信大,您好,不好意思,因為最近在開發產品關係,採用了struts2這個框架,覺得這個框架很好用呢
因在Study這個框架同時,也有來看您的講解,寫得非常好,但是看到了這篇有個問題想請教一下?
就是當我們在做logout同時,會將登入者的資訊從session給清除掉,那假如今天有a b c 這三位使用者登入,假如a執行了logout動作後,b想要提交資料時,會送出一個action,然後在送出時,攔截器會先進行檢查動作,但b在提交前,a已經先執行logout動作了,那這樣的話b所提交的動作會繼續完成,還是說會被重新登入呢??

Silver Su 提到...

您提到的狀況,a 跟 b 是不會彼此影響的。只有一個情況會有例外,就是 a 跟 b 是用同一台 PC 的同一個 browser 程式,開了不同的分頁才有可能!
即使 a 跟 b 使用同一個帳號登入你的系統,除非你有作帳號連線數的控管,否則這兩個人也不會互相影響~
session 的機制是透過 web 與 browser 之間的 cookies 來完成的,也就是 browser 會與 web 之間有一個關聯的 key 在,這樣 web 端才知道 browser 還跟他有連線關係。所以不同的 browser 軟體與 Web 之間會有不同的 cookies 來紀錄連線。
如果您還是有問題,請您繼續發問!謝謝!

didifong 提到...

阿信大,您好:
謝謝您的回覆阿,小弟在系統上有同時做測試了,的確是不會引響,實在是很謝謝您的回覆阿,覺得自己問了一個很白痴的問題,謝謝您的回答了

Silver Su 提到...

您客氣了!您的問題我也曾經問過,人總會有第一次的!當自己的知識累積的更多時,回想過去的問題,總是會讓自己會心一笑的!
有問題歡迎您繼續發問~