一,Struts2攔截器簡述
· 攔截器(Interceptor)是Struts2的核心組成部分。
· Struts2很多功能都是構建在攔截器之上的,例如文件的上傳和下載,國際化,數據類型的轉換和數據校驗等。
· Struts2攔截器在訪問某個Action方法之前或之後實施攔截。
· Struts2攔截器是可插拔的,攔截器是AOP(面向切面編程)的一種實現。
· 攔截器棧(Interceptor Stack):將攔截器按一定順序聯結成一條鏈。在訪問被攔截的方法時,Struts2攔截器鏈中的攔截器就會按其之前定義的順序被依次調用,如下圖所示:
Strut2默認的攔截器棧
<interceptor-stack name="defaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="servletConfig"/>
<interceptor-ref name="i18n"/>
<interceptor-ref name="prepare"/>
<interceptor-ref name="chain"/>
<interceptor-ref name="scopedModelDriven"/>
<interceptor-ref name="modelDriven"/>
<interceptor-ref name="fileUpload"/>
<interceptor-ref name="checkbox"/>
<interceptor-ref name="multiselect"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="actionMappingParams"/>
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,parameters\...*</param>
</interceptor-ref>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="debugging"/>
</interceptor-stack>
攔截器棧源碼解析
public String invoke() throws Exception {
String profileKey = "invoke: ";
try {
UtilTimerStack.push(profileKey);
if (executed) {
throw new IllegalStateException("Action has already executed");
}
//根據Struts2的配置文件中所配置的攔截器棧,按順序調用各個攔截器,當調用完某個攔截器的方法後,該攔截器最後會執行調用攔截器棧的invoke()方法即 ActionInvocation 的 invoke() 方法
if (interceptors.hasNext()) {
final InterceptorMapping interceptor = interceptors.next();
String interceptorMsg = "interceptor: " + interceptor.getName();
UtilTimerStack.push(interceptorMsg);
try {
resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
}
finally {
UtilTimerStack.pop(interceptorMsg);
}
} else {
resultCode = invokeActionOnly();
}
二,Struts2自帶攔截器
三,詳述Params攔截器,ModelDrivenInterceptor及PreparableInterceptor
(1)Params攔截器
ParametersInterceptor攔截器將把表單字段映射到ValueStack棧的棧頂對象的各個屬性中.如果某個字段在模型裏沒有匹配的屬性在,Param攔截器將嘗試ValueStack棧中的下一個對象.
(2)ModelDrivenInterceptor
如果Action類實現了ModelDriven接口,該攔截器將把ModelDriven接口的getModels()方法返回的對象置於棧頂
Action 實現 ModelDriven 接口後的運行流程
① 先會執行 ModelDrivenInterceptor 的 intercept 方法.
public String intercept(ActionInvocation invocation) throws Exception {
//獲取 Action 對象: EmployeeAction 對象, 此時該 Action 已經實現了 ModelDriven 接口
//public class EmployeeAction implements RequestAware, ModelDriven<Employee>
Object action = invocation.getAction();
//判斷 action 是否是 ModelDriven 的實例
if (action instanceof ModelDriven) {
//強制轉換爲 ModelDriven 類型
ModelDriven modelDriven = (ModelDriven) action;
//獲取值棧
ValueStack stack = invocation.getStack();
//調用 ModelDriven 接口的 getModel() 方法
//即調用 EmployeeAction 的 getModel() 方法
/*
public Employee getModel() {
employee = new Employee();
return employee;
}
*/
Object model = modelDriven.getModel();
if (model != null) {
//把 getModel() 方法的返回值壓入到值棧的棧頂. 實際壓入的是 EmployeeAction 的 employee 成員變量
stack.push(model);
}
if (refreshModelBeforeResult) {
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
}
}
return invocation.invoke();
}
②執行 ParametersInterceptor 的 intercept 方法: 把請求參數的值賦給棧頂對象對應的屬性. 若棧頂對象沒有對應的屬性, 則查詢
值棧中下一個對象對應的屬性…
③
3). 注意: getModel 方法不能提供以下實現. 的確會返回一個 Employee 對象到值棧的棧頂. 但當前 Action 的 employee 成員變量卻是 null.
public Employee getModel() {
return new Employee();
}
(3)PreparableInterceptor
· PreparableInterceptor攔截器負責準備getModel()方法準備model
· 若Action實現了Preparable接口,則Action方法需實現prepare()方法
· PrepareInterceptor攔截器將調用prepare()方法,prepareActionMethodName()方法或prepareDoActionMethodName()方法
· PrepareInterceptor攔截器根據firstCallPrepareDo屬性決定獲取prepareActionMethodName,prepareDoActionMethodName的順序.默認情況下先獲取prepareActionMethodName()方法,若沒有該方法就尋找prepareDoActionMethodName()。
·PrepareInterceptor攔截器會根據alwaysInvokePrepare屬性決定是否執行prepare()方法
注:可以爲每一個 ActionMethod 準備 prepare[ActionMethdName] 方法, 而拋棄掉原來的 prepare() 方法將 PrepareInterceptor 的 alwaysInvokePrepare 屬性置爲 false, 以避免 Struts2 框架再調用 prepare() 方法。配置如下:
<interceptors>
<interceptor-stack name="teststack">
<interceptor-ref name="paramsPrepareParamsStack">
<param name="prepare.alwaysInvokePrepare">false</param>
</interceptor-ref>
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="teststack"/>
· 源碼解析
public String doIntercept(ActionInvocation invocation) throws Exception {
//獲取 Action 實例
Object action = invocation.getAction();
//判斷 Action 是否實現了 Preparable 接口
if (action instanceof Preparable) {
try {
String[] prefixes;
//根據當前攔截器的 firstCallPrepareDo(默認爲 false) 屬性確定 prefixes
if (firstCallPrepareDo) {
prefixes = new String[] {ALT_PREPARE_PREFIX, PREPARE_PREFIX};
} else {
prefixes = new String[] {PREPARE_PREFIX, ALT_PREPARE_PREFIX};
}
//若爲 false, 則 prefixes: prepare, prepareDo
//調用前綴方法.
PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes);
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
throw (Exception) cause;
} else if(cause instanceof Error) {
throw (Error) cause;
} else {
throw e;
}
}
//根據當前攔截器的 alwaysInvokePrepare(默認是 true) 決定是否調用 Action 的 prepare 方法
if (alwaysInvokePrepare) {
((Preparable) action).prepare();
}
}
return invocation.invoke();
}
PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes) 方法:
public static void invokePrefixMethod(ActionInvocation actionInvocation, String[] prefixes) throws InvocationTargetException, IllegalAccessException {
//獲取 Action 實例
Object action = actionInvocation.getAction();
//獲取要調用的 Action 方法的名字(update)
String methodName = actionInvocation.getProxy().getMethod();
if (methodName == null) {
// if null returns (possible according to the docs), use the default execute
methodName = DEFAULT_INVOCATION_METHODNAME;
}
//獲取前綴方法
Method method = getPrefixedMethod(prefixes, methodName, action);
//若方法不爲 null, 則通過反射調用前綴方法
if (method != null) {
method.invoke(action, new Object[0]);
}
}
PrefixMethodInvocationUtil.getPrefixedMethod 方法:
public static Method getPrefixedMethod(String[] prefixes, String methodName, Object action) {
assert(prefixes != null);
//把方法的首字母變爲大寫
String capitalizedMethodName = capitalizeMethodName(methodName);
//遍歷前綴數組
for (String prefixe : prefixes) {
//通過拼接的方式, 得到前綴方法名: 第一次 prepareUpdate, 第二次 prepareDoUpdate
String prefixedMethodName = prefixe + capitalizedMethodName;
try {
//利用反射獲從 action 中獲取對應的方法, 若有直接返回. 並結束循環.
return action.getClass().getMethod(prefixedMethodName, EMPTY_CLASS_ARRAY);
}
catch (NoSuchMethodException e) {
// hmm -- OK, try next prefix
if (LOG.isDebugEnabled()) {
LOG.debug("cannot find method [#0] in action [#1]", prefixedMethodName, action.toString());
}
}
}
return null;
}
四, 使用 paramsPrepareParamsStack 攔截器棧後的運行流程
1). paramsPrepareParamsStack 和 defaultStack 一樣都是攔截器棧. 而 struts-default 包默認使用的是defaultStack
2).可以在 Struts 配置文件中通過以下方式修改使用的默認的攔截器棧
<default-interceptor-ref name="paramsPrepareParamsStack"></default-interceptor-ref>
3). paramsPrepareParamsStack 攔截器在於
params -> modelDriven -> params
所以可以先把請求參數賦給 Action 對應的屬性, 再根據賦給 Action 的那個屬性值決定壓到值棧棧頂的對象, 最後再爲棧頂對象的屬性賦值.
4).運行流程:
①Params攔截器首先給action的相關參數賦值,如id.
②Params攔截器執行prepare()方法,prepare()方法會根據參數,如id,去調用業務邏輯,設置model對象
③modelDriven攔截器將model對象壓入Value Stack,這裏的model對象就是在prepare中創建的
④Params攔截器再將參數賦值給model對象
⑤執行action的業務邏輯
五,表單的重複提交問題
(1)什麼是表單的重複提交:
在不刷新表單頁面的前提下:
*多次點擊提交按鈕;
*已經提交成功,按“回退”後,再點擊”提交按鈕”;
*在控制器響應頁面的形式爲轉發的情況下,若已經提交成功,然後點擊刷新;
注:
①若刷新表單頁面,再提交表單不算重複提交;
②若使用的是redirect的響應類型,已經提交成功後,再點擊”刷新”,不是表單的重複提交;
(2) struts2解決表單的重複提交問題
詳述:
· Struts 提供的 token 標籤可以用來生成一個獨一無二的標記. 這個標籤必須嵌套在 form 標籤的內部使用, 它將在表單裏插入一個隱藏字段並把標記值(隱藏域的字段的值)保存在HttpSession 對象裏.
· Token 標籤必須與 Token 或 TokenSession 攔截器配合使用, 這兩個攔截器都能對標記進行處理.
· Token 攔截器在遇到重複提交情況時, 會返回 invalid.token 結果並加上一個 Action 錯誤. 這種錯誤默認的消息是: The form has already been processed or no token was supplied, please try again.
· TokenSession 攔截器採取的做法只是阻斷後續的提交, 用戶將看到同樣的響應,但實際上並沒有重複提交
(3). 使用 Token 或 TokenSession 攔截器.
> 這兩個攔截器均不在默認的攔截器棧中, 所以需要手工配置一下
> 若使用 Token 攔截器, 則需要配置一個 token.valid 的 result
> 若使用 TokenSession 攔截器, 則不需要配置任何其它的 result
(4). Token VS TokenSession
> 都是解決表單重複提交問題的
> 使用 token 攔截器會轉到 token.valid 這個 result
> 使用 tokenSession 攔截器則還會響應那個目標頁面, 但不會執行 tokenSession 的後續攔截器. 就像什麼都沒發生過一樣!
(5). 可以使用 s:actionerror 標籤來顯示重複提交的錯誤消息.
該錯誤消息可以在國際化資源文件中覆蓋. 該消息可以在 struts-messages.properties 文件中找到
struts.messages.invalid.token=^^The form has already been processed or no token was supplied, please try again.