Struts2(十)---攔截器

一,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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章