29 July 2005

Redirect After Post 是 web application 常用的技巧。簡單講就是 user submit form、做完 transaction、然後 "redirect" 到成功網頁。使用 http redirect 可以防止 browser refresh button 所引起的二次 submit (如果你不知道這是什麼,可以自己試一遍 submit form 後,再按 refresh 看看)

上面附的連結裡,他使用的是 Post-Redirect-Get pattern -- post 完之後,接著 redirect,然後成功網頁再去 Get 資料(也許是從資料庫),而且他建議所有的網頁都這樣寫。個人認為這樣太過火了,因為真正會出現 二次 submit 問題多半是 "最後一個 submit網頁" (例如結算購物車的網頁),在這裡使用 Redirect After Post 就夠了,其他的網頁就算出現二次 submit,由於不會動到資料庫,頂多像是購物車的商品數量會加 1 而已,最後結算前 user 還可以再更改,所以這並不構成問題。

好,回到正題。在 Struts 裡做 RedirectAfterPost 很簡單:

public class SaveAccountAction {
    public ActionForward execute(ActionMapping mapping...) {
         Account account = (Account)request.getAttribute("account") ;
         getAccountService().saveAccount(account) ;
         return mapping.findForward("success") ;
    }
}
in struts-config.xml:
<action path="/account/save"
        type="mycom.SaveAccountAction" >
    <!-- 設定 redirect="true" 即可 --> 
    <forward redirect="true" name="success" path="/accout/success.jsp" >
</action>
user submit /account/save.do 後,便 redirect 到 /account/success.jsp。這大家應該沒啥問題... 不過這帶出另一個問題:如何在 /account/success.jsp 裡面顯示訊息-- 已建立 XXX 帳號。在還未使用 redirect 之前,我們可以將 ActionMessage 儲在 request 裡:
public ActionForward execute(ActionMapping mapping...) {
     Account account = (Account)request.getAttribute("account") ;
     getAccountService().saveAccount(account) ;

     //create messages
     ActionMessages messages = new ActionMessages() ;
     messages.add(ActionMessages.GLOBAL_MESSAGE
                , new ActionMessage("account.save.success"), account.getName());

     //save ActionMessage in request, 不過在 redirect 時不適用
     saveMessages(request, messges) ;
     return mapping.findForward("success") ;
}

in /account/success.jsp:

    <html:messages id="msg" message="true">
         ${msg}
    </html:messages>

由於 redirect 會產生第二個 request,所以 saveMessages(request, messages) 是沒有作用的。為解決這個問題,Struts 1.2 追加一個 method:

     HttpSession session = request.getSession(false) ;
     saveMessages(session, messges) ; //將 ActionMessage 儲存到 session 裡

有了這個之後,redirect 之後也可以看到訊息了。聰明的看官應該可以發現這種做法有個問題 (第三個問題了...) 就是什麼時候該移除掉這個 ActionMessage ? 難道一直放在 session 裡嗎?對於這個問題,Struts 有個解法 -- 當你下次進入 Struts 時 (也就是經由 *.do 的path進入ActionServlet),Struts 會將 session 裡所有的 ActionMessage 移除掉,比方說我們的 success.jsp 裡這樣寫:

in /account/success.jsp

    <!-- show message here -->
    <html:messages id="msg" message="true">
         ${msg}
    </html:messages>

    <!-- 使用者若點了下面的 link,ActionMessage 便會自動清除。
      tag 展開後為:<a href="/yourContext/account/home.do" >Back to Top</a>
      -->
    <html:link action="/account/home">Back to Top</a>

in struts-config.xml:

    <!-- 替 /account/home.jsp 做一個 logical path -->
    <action path="/account/home"
            forward="/account/home.jsp" />

上面寫的 Back to Top 只是個範例,只要使用者離開 success.jsp 網頁時,是經由 *.do 的 path ,ActionServlet 便會移除掉 ActionMessages... 所以... 這又帶出了下個問題 (第四個了 -_-;;) -- 不要讓 page 裡出現 *.jsp 的連結:

<!-- 錯誤範例 1,直接提供 .jsp link -->
  <a href="/yourContext/account/home.jsp" >Back to Top</a>

<!-- 錯誤範例 2,直接forward 到 .jsp link -->

  in struts-config.xml:

      <forward name="home"
               path="/account/home.jsp" />

  in /account/home.jsp:

      <html:link foward="home">Back to Top</a>

      <!-- 注意上面 tag 展開後為:
           <a href="/yourContext/account/home.jsp" >Back to Top</a> -->

上述的兩種情形,第一種應該不常見,第二種就要小心了,因為雖然沒在 page 裡直接寫 .jsp 的URI,但是 <html:link foward="home" 展開後卻會變成 .jsp。所以最好將所有要在網頁上提供連結的 .jsp 都 mapping 成 action。(其實這是 Struts 的基本精神,它希望所有的真實 URI 都 mapping 成 logical 的 path)

OK,總結一下:

  • 使用 RedirectAfterPost 解決二次 submit 問題
  • 使用 saveMessages(session, messages) 將 ActionMessage 儲存在 session
  • 使用 <html:messages> 在 jsp 裡顯示訊息
  • 不要讓 *.jsp 這種網址出現在 page 上,全部都要使用 *.do (使用 tag-- <html:link action="/foo/bar" />