理解Spring MVC Model Attribute和Session Attribute

ImportNew發表於2015-10-11

作為一名 Java Web 應用開發者,你已經快速學習了 request(HttpServletRequest)和 session(HttpSession)作用域。在設計和構建 Java Web 應用時,理解這些作用域,如何將資料與物件和這些作用域互動是十分重要的。【在 StackOverflow 上有一篇文章可以幫助你快速瞭解 request 和 session 作用域】

SPRING MVC 作用域

當我開始用 Spring MVC 編寫 Web 應用時,我發現 Spring model 和 session attribute 有一點神祕,尤其當它們與我熟知的 HTTP request 和 session 作用域互動時。一個 Spring model 元素可以從我的 session 或者 request 中找到嗎?如果是這樣的話,我該如何控制?在這篇文章中,我希望講解清楚 Spring MVC 的 model 與 session 是如何工作的。

SPRING 的 @MODELATTRIBUTE

有幾種方法將資料或物件新增到 Spring 的 model 中。一般來說,資料或物件是通過 controller 層的一個註解新增進 Spring 的 model 中。在下面的例子中,使用 @ModelAttribute 新增一個名為 MyCommandBean 的例項給 key 值為『myRequestObject』的 model。

@CONTROLLER
PUBLIC CLASS MYCONTROLLER {

    @MODELATTRIBUTE("MYREQUESTOBJECT")
    PUBLIC MYCOMMANDBEAN ADDSTUFFTOREQUESTSCOPE() {
        SYSTEM.OUT.PRINTLN("INSIDE OF ADDSTUFFTOREQUESTSCOPE");
        MYCOMMANDBEAN BEAN = NEW MYCOMMANDBEAN("HELLO WORLD",42);
        RETURN BEAN;
    }

    @REQUESTMAPPING("/DOSOMETHING")
    PUBLIC STRING REQUESTHANDLINGMETHOD(MODEL MODEL, HTTPSERVLETREQUEST REQUEST) {
        SYSTEM.OUT.PRINTLN("INSIDE OF DOSOMETHING HANDLER METHOD");

        SYSTEM.OUT.PRINTLN("--- MODEL DATA ---");
        MAP MODELMAP = MODEL.ASMAP();
        FOR (OBJECT MODELKEY : MODELMAP.KEYSET()) {
            OBJECT MODELVALUE = MODELMAP.GET(MODELKEY);
            SYSTEM.OUT.PRINTLN(MODELKEY + " -- " + MODELVALUE);
        }

        SYSTEM.OUT.PRINTLN("=== REQUEST DATA ===");
        JAVA.UTIL.ENUMERATION REQENUM = REQUEST.GETATTRIBUTENAMES();
        WHILE (REQENUM.HASMOREELEMENTS()) {
            STRING S = REQENUM.NEXTELEMENT();
            SYSTEM.OUT.PRINTLN(S);
            SYSTEM.OUT.PRINTLN("==" + REQUEST.GETATTRIBUTE(S));
        }

        RETURN "NEXTPAGE";
    }

         //  ... THE REST OF THE CONTROLLER
}

在一個到達的 request 中,任何被 @ModelAttribute 註解的方法都會在 controller handler method 之前呼叫(就像上面例子中的 requestHandlingMethod 一樣)。這些方法會趕在 handler method 執行之前將資料新增進一個 java.util.Map,然後加入 Spring model 中。可以用一個示例操作展示出來。我建立了兩個 JSP 頁面:index.jsp 和 nextpage.jsp。index.jsp 上的一個連結用於向 MyController 中的 requestHandlingMethod() 應用觸發器傳送一個 request。上面的程式碼中,requestHandlingMethod() 將『nextpage』作為下個檢視的邏輯名返回,其在這個例子中會處理為 nextpage.jsp。

理解Spring MVC Model Attribute和Session Attribute

當這個小小的網址被修改為這種形式後,controller 的 System.out.println 展現了 @ModelAttribute 方法是如何在 handler method 之前執行的。同時也展現了 MyCommandBean 建立和加入 Spring model,並在 handler method 中可用的過程。

INSIDE OF ADDSTUFFTOREQUESTSCOPE
INSIDE OF DOSOMETHING HANDLER METHOD
--- MODEL DATA ---
MYREQUESTOBJECT -- MYCOMMANDBEAN [SOMESTRING=HELLO WORLD, SOMENUMBER=42]
=== REQUEST DATA ===
ORG.SPRINGFRAMEWORK.WEB.SERVLET.DISPATCHERSERVLET.THEME_SOURCE
==WEBAPPLICATIONCONTEXT FOR NAMESPACE 'DISPATCHER-SERVLET': STARTUP DATE [SUN OCT 13 21:40:56 CDT 2013]; ROOT OF CONTEXT HIERARCHY
ORG.SPRINGFRAMEWORK.WEB.SERVLET.DISPATCHERSERVLET.THEME_RESOLVER
==ORG.SPRINGFRAMEWORK.WEB.SERVLET.THEME.FIXEDTHEMERESOLVER@204AF48C
ORG.SPRINGFRAMEWORK.WEB.SERVLET.DISPATCHERSERVLET.CONTEXT
==WEBAPPLICATIONCONTEXT FOR NAMESPACE 'DISPATCHER-SERVLET': STARTUP DATE [SUN OCT 13 21:40:56 CDT 2013]; ROOT OF CONTEXT HIERARCHY
ORG.SPRINGFRAMEWORK.WEB.SERVLET.HANDLERMAPPING.PATHWITHINHANDLERMAPPING
==DOSOMETHING.REQUEST
ORG.SPRINGFRAMEWORK.WEB.SERVLET.HANDLERMAPPING.BESTMATCHINGPATTERN
==/DOSOMETHING.*
ORG.SPRINGFRAMEWORK.WEB.SERVLET.DISPATCHERSERVLET.LOCALE_RESOLVER
==ORG.SPRINGFRAMEWORK.WEB.SERVLET.I18N.ACCEPTHEADERLOCALERESOLVER@18FD23E4

現在問題變為了『Spring model 的資料儲存在哪裡?』是儲存在標準 Java request 作用域中麼?答案是 —— 對的。。。就最終而言的話。就像你從上面的輸出中讀到的,MyCommandBean 在 model 中,但當 handler method 執行時還不在 request 物件中。的確,handler method 執行之後,下個檢視(本例中為 nextpage.jsp)顯示之前為止,Spring 都沒有將 model 資料作為 attribute 加入 request 中。

理解Spring MVC Model Attribute和Session Attribute

也可以通過輸出儲存在 index.jsp 與 nextpage.jsp 的 HttpServletRequest 中的 attribute 展現出來。我在兩個頁面中都佈置了一個 JSP 程式碼塊(如下面所示),用以展現 HttpServletRequest 的 attribute。

<HR />
<H3>REQUEST SCOPE (KEY==VALUES)</H3>
<%
    JAVA.UTIL.ENUMERATION<STRING> REQENUM = REQUEST.GETATTRIBUTENAMES();
    WHILE (REQENUM.HASMOREELEMENTS()) {
        STRING S = REQENUM.NEXTELEMENT();
        OUT.PRINT(S);
        OUT.PRINTLN("==" + REQUEST.GETATTRIBUTE(S));
%><BR />
<%
    }
%>

當應用啟動,index.jsp 載入完畢,你可以看到在 request 作用域中沒有 attribute。

理解Spring MVC Model Attribute和Session Attribute

在本例中,當『do something』被點選時執行 MyController 的 handler method,然後會跳轉並展示 nextpage.jsp。而 nextpage.jsp 中已經編寫了相同的 JSP 程式碼塊,同樣提供了 request 作用域中的 attribute。瞧,當 nextpage.jsp 渲染後,顯示出在 controller 中建立的 MyCommandBean model 被加進 HttpServletRequest 作用域中了!Spring model attribute 的鍵值『myRequestObject』被複制後用作 request attribute 的鍵值。

理解Spring MVC Model Attribute和Session Attribute

所以下一個檢視呈現之前,Spring model 資料已經在 handler method 執行之前(或者之間)被拷貝給了 HttpServletRequest。

使用 SPRING MODEL 與 REQUEST 的原因

你或許想知道為什麼 Spring 使用 model attribute。為何不直接把資料加到 request 物件裡?我在 Rod Johnson 等人的書籍《Professional Java Development with the Spring Framework》中找到了答案。這本書關於 Spring API 的部分有一點過時(基於 Spring 2.0 編寫),但是我發現該書提供了一些對於 Spring 引擎執行的擴充套件解釋。下面是書中 model 元素部分的引用:

直接將元素加入 HttpServletRequest(像 request attributes 一樣)看起來就像在服務同樣的目標。這樣做的理由是當看到我們為 MVC 框架設定的 requirements 時,能夠更明確。它應儘可能與檢視無關,這意味著我們可以合併檢視技術,並不受 HttpServletRequest 的束縛。

SPRING 的 @SESSIONATTRIBUTES

所以現在你知道了 Spring 如何管理 model 資料,與如何連線標準的 Http request attribute 資料。那麼關於 Spring 的 session 資料呢?

Spring 的 @SessionAttribute 在 controller 中用來指定哪一個 model attributes 需要儲存到 session。事實上,Spring 文件宣告瞭 @SessionAttributes 註解『列舉需要顯式地儲存 session 或一些互動用的儲存空間內的 model attributes 名稱。』另外說一下,『一些互動儲存空間』表明了 Spring MVC 試圖保持與技術無關聯的設計思想。

事實上,@SessionAttributes 允許你做的就是告訴 Spring 哪一個 model attributes 將在檢視展現之前一同拷貝給 HttpSession。關於這一點同樣可以用一個簡短的程式碼來展示。

在 index.jsp 和 nextpage.jsp 中,我新增了額外的 JSP 程式碼塊,使其顯示 HttpSession attributes。

<H3>SESSION SCOPE (KEY==VALUES)</H3>
<%
  JAVA.UTIL.ENUMERATION<STRING> SESSENUM = REQUEST.GETSESSION()
    .GETATTRIBUTENAMES();
  WHILE (SESSENUM.HASMOREELEMENTS()) {
    STRING S = SESSENUM.NEXTELEMENT();
    OUT.PRINT(S);
    OUT.PRINTLN("==" + REQUEST.GETSESSION().GETATTRIBUTE(S));
%><BR />
<%
  }
%>

我使用 @SessionAttributes 註解 MyController,使其將同一個 model attributes(myRequestObject)放入 Spring session 中。

@CONTROLLER
@SESSIONATTRIBUTES("MYREQUESTOBJECT")
PUBLIC CLASS MYCONTROLLER {
  ...
}

另外在 controller 的 handler method 中新增程式碼顯示 HttpSession 中的 attributes(就像顯示 HttpServletRequest 中的 attributes 一樣)。

@SUPPRESSWARNINGS("RAWTYPES")
@REQUESTMAPPING("/DOSOMETHING")
PUBLIC STRING REQUESTHANDLINGMETHOD(MODEL MODEL, HTTPSERVLETREQUEST REQUEST, HTTPSESSION SESSION) {
  SYSTEM.OUT.PRINTLN("INSIDE OF DOSOMETHING HANDLER METHOD");

  SYSTEM.OUT.PRINTLN("--- MODEL DATA ---");
  MAP MODELMAP = MODEL.ASMAP();
  FOR (OBJECT MODELKEY : MODELMAP.KEYSET()) {
    OBJECT MODELVALUE = MODELMAP.GET(MODELKEY);
    SYSTEM.OUT.PRINTLN(MODELKEY + " -- " + MODELVALUE);
  }

  SYSTEM.OUT.PRINTLN("=== REQUEST DATA ===");
  JAVA.UTIL.ENUMERATION<STRING> REQENUM = REQUEST.GETATTRIBUTENAMES();
  WHILE (REQENUM.HASMOREELEMENTS()) {
    STRING S = REQENUM.NEXTELEMENT();
    SYSTEM.OUT.PRINTLN(S);
    SYSTEM.OUT.PRINTLN("==" + REQUEST.GETATTRIBUTE(S));
  }

  SYSTEM.OUT.PRINTLN("*** SESSION DATA ***");
  ENUMERATION<STRING> E = SESSION.GETATTRIBUTENAMES();
  WHILE (E.HASMOREELEMENTS()){
    STRING S = E.NEXTELEMENT();
    SYSTEM.OUT.PRINTLN(S);
    SYSTEM.OUT.PRINTLN("**" + SESSION.GETATTRIBUTE(S));
  }

  RETURN "NEXTPAGE";
}

現在,我們可以看見加上 @SessionAttributes 註解後,Spring MVC 處理一個 HTTP 請求之前、之間和之後的 session 物件情況。下面顯示了結果。首先,當 index.jsp 顯示時(請求被 Spring MVC 傳送和處理之前),我們可以看見 HttpServletRequest 和 HttpSession 都沒有 attribute 資料。

理解Spring MVC Model Attribute和Session Attribute

handler method 執行時(requestHandlingMethod),你可以看見 MyCommandBean 被新增進 Spring model attributes,但是還沒有加入 HttpServletRequest 或 HttpSession 作用域。

理解Spring MVC Model Attribute和Session Attribute

但是 handler method 執行後和 nextpage.jsp 顯示時,你可以看見 model attribute 資料(MyCommandBean)已經作為一個 attribute 被複制給了 HttpServletRequest 和 HttpSession(擁有相同的 attribute key)。

理解Spring MVC Model Attribute和Session Attribute

控制 SESSION ATTRIBUTES

現在你已經理解了 Spring model 和 session attribute 資料如何新增進 HttpServletRequest 與 HttpSession。或許又開始關心怎麼管理 Spring session 中的資料。Spring 提供了一個方法移除 Spring session attributes,同時也會從 HttpSession 中移除(不需要刪除整個 HttpSession)。簡單地將一個 Spring SessionStatus 物件作為引數加入一個 controller handler method 中。在此方法中,使用 SessionStatus 物件結束這個 Spring session。

@REQUESTMAPPING("/ENDSESSION")
PUBLIC STRING NEXTHANDLINGMETHOD2(SESSIONSTATUS STATUS){
  STATUS.SETCOMPLETE();
  RETURN "LASTPAGE";
}

總結

希望這篇文章能夠幫助你理解 Spring model 和 session attributes。這並不神奇,僅僅是一個理解 HttpSession 和 HttpServletRequest 如何儲存 Spring model 和 session attributes 的問題。我已經將展示用的程式碼放在了 Intertech Web site 上。如果你對繼續探索與理解 Spring model 和 session 感興趣,儘管從這裡下載吧。

相關文章