Portlet MVC框架
16.1. 介紹
Spring不僅支持傳統(基於Servlet)的Web開發,也支持JSR-168 Portlet開發。 Portlet MVC框架盡可能多地采用Web MVC框架,使用相同的底層表現層抽象和整合技術。所以, 在繼續閱讀本章前,務必溫習Chapter 13, Web框架和Chapter 14, 集成視圖技術兩章。
![]() |
Note |
---|---|
請牢記,在Spring MVC中的概念和Spring Porlet MVC中的相同的同時,JSR-168 Portlet 獨特的工作流程造成了一些顯著的差異。 |
Porlet工作流程和Servlet的主要差異在於,Portlet的請求處理有兩個獨特 的階段:動作階段和顯示階段。動作階段會有“后台”數據改變或動作的代碼,這些代碼 只會執行一次。顯示階段會產生用戶每次刷新時的看到的顯示內容。重要的是, 在單個請求的整個處理過程中,動作階段只會被執行一次,而顯示階段可能會被執行多次。 這就提供了(並且要求)在改變系統持久狀態的活動和產生顯示內容的活動之間 有一個清晰的分層。
這種兩階段的請求處理是JSR-168規范的一個優點,比如,可以自動地更新動態 的搜索結果,不需要用戶特意去再次執行搜索。許多其它的Portlet MVC框架試圖向開 發人員徹底隱藏這種兩階段處理,讓框架看上去盡可能和傳統的Servlet開發相同 - 在我們 看來,這種方式去掉了使用Portlet的一個主要好處,所以在Spring Portlet MVC 框架里分離的兩階段處理被保留了下來,這主要表現在,Servlet版本的MVC類將只 有一個方法來處理請求,而Portlet版本的MVC類里將會有兩個方法:一個用在動作 階段,另一個用在顯示階段。比如,在Servlet版本的 AbstractController有 handleRequestInternal(..)方法,Portlet版本的 AbstractController有 handleActionRequestInternal(..)和 handleRenderRequestInternal(..)方法。
這個框架是圍繞着分發器 DispatcherPortlet設計的,分發器把請求轉發給處理 器。和Web框架的DispatcherServlet一樣, 這個框架還有可配置的處理器映射和視圖解析,同時也支持文件上傳。
Portlet MVC不支持本地化解析和主題解析 - 它們是portal/portlet容器 的范疇,並不適合放在Spring框架里。但是,Spring里所有依賴本地化(比如消息的 國際化)仍舊可以工作,因為DispatcherPortlet在以 DispatcherServlet相同的方式暴露當前的本地化信息。
缺省的處理器是一個非常簡單的 Controller接口,它提供了兩個方法:
-
void handleActionRequest(request,response)
-
ModelAndView handleRenderRequest(request,response)
這個框架包含了許多相同的控制器實現層次,比如, AbstractController, SimpleFormController等。它在數據綁定、命令對象使用、 模型處理和視圖解析等方面和Servlet框架相同。
這個框架利用了一個特殊的橋Servlet ViewRendererServlet來使用Servlet框架里的視圖顯示 功能,這樣,Portlet請求就被轉化為Servlet請求,Portlet視圖能夠以通常的 Servlet底層代碼來顯示。這意味着,在Portlet里仍能使用當前所有的顯示方法, 如JSP、Velocity等。
Spring Portlet MVC支持Web Bean,這些Bean的生命周期在於當前的HTTP請求 或HTTP Session(一般的和全局的)里,這不是 框架自身的特性,而是由使用的容器的 WebApplicationContext提供的。 Section 3.4.3, “其他作用域”詳細地描述了這些Bean的作用范圍。
![]() |
Tip | ||
---|---|---|---|
|
Portlet MVC是一個請求驅動的Web MVC框架,它圍繞着Portlet設計,把請求 轉發給控制器,提供了便利的Porltet應用開發功能。而且,Spring的 DispatcherPortlet功能遠遠不止這些,它和Spring ApplicationContext完全集成,使得開發人員 能夠使用Spring其它部分的每個功能。
DispatcherPortlet和一般的Portlet一樣, 在Web應用的portlet.xml中聲明:
<portlet> <portlet-name>sample</portlet-name> <portlet-class>org.springframework.web.portlet.DispatcherPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> <portlet-mode>view</portlet-mode> </supports> <portlet-info> <title>Sample Portlet</title> </portlet-info> </portlet>
現在需要配置DispatcherPortlet。
在Portlet MVC框架里,每個 DispatcherPortlet都有自己的 WebApplicationContext,它接管了所有在根 WebApplicationContext定義的Bean。我們可以 在Portlet作用范圍內對這些Bean進行重載,重載后的Bean可以定義成對於特定 的Portlet實例可見。
在初始化 DispatcherPortlet時,框架會在Web應用的WEB-INF 目錄下尋找 [portlet-name]-portlet.xml,生成在其中定義的Bean(會覆蓋 在全局范圍里名字相同的Bean的定義)。
DispatcherPortlet用到的配置文件位置 可以通過Portlet初始化參數來修改(下面有詳細的描述)。
Spring的DispatcherPortlet會用一些特殊的Bean 來處理請求和顯示視圖。這些Spring包含的Bean和其它的Bean一樣,可以在 WebApplicationContext里進行配置。每 個Bean下面都會有詳細的描述。這里,只是讓你知道它們, 我們繼續討論DispatcherPortlet。大多數的Bean都有缺省 配置,所以你不需要擔心它們的配置。
Table 16.1. WebApplicationContext 里的特殊的Bean
名詞 | 解釋 |
---|---|
處理器映射 | (Section 16.5, “處理器映射”) 一個前置和后置的處理器以及控制器的列表,這些控制器 通過匹配特定的條件(比如,由控制器指定的Portlet模式), 從而得到執行。 |
控制器 | (Section 16.4, “控制器”)是MVC的一員, 是提供(或至少可以訪問)具體功能的Bean |
視圖解析器 | (Section 16.6, “視圖和它們的解析”) 能夠將 視圖名字對應到視圖定義。 |
分段(multipart)解析器 | (Section 16.7, “Multipart文件上傳支持”) 能夠處理 從HTML表單上傳的文件 |
處理器異常解析器 | (Section 16.8, “異常處理”) 能夠將異常對應到視圖,或實現某種復雜的異常處理代碼 |
在DispatcherPortlet配置好后,請求進入到特定 DispatcherPortlet時,它開始處理。下面描述了 DispatcherPortlet處理請求的完整過程:
-
PortletRequest.getLocale()返回 的Locale綁定在請求上,這使得在處理請求時(如顯示視圖、准備數據等), 代碼能夠使用Locale。
-
如果在ActionRequest里 指定了分段解析器,框架會在請求里尋找分段,如果找到了, 會把它們包裝在MultipartActionRequest 里,供在后續處理中使用。(關於分段處理的進一步信息見Section 16.7, “Multipart文件上傳支持” )。
-
尋找合適的處理器。如果找到了,這個處理器關聯的執行鏈 (前置處理器、后置處理器和控制器)會被按序執行來准備模型。
-
如果有模型返回,視圖通過視圖解析器進行顯示,視圖解析器是在 WebApplicationContext配置好的。如果沒有模型 返回(可能由於預處理器或后處理器攔截了請求,比如安全原因),就不會有視圖顯示 因為有可能請求已經被處理了。
在WebApplicationContext里 定義的異常處理解析器能夠捕獲在處理請求時可能拋出的異常,借助這些解析器, 我們可以對在捕獲特定異常時的操作進行自定義。
通過在portlet.xml文件里增加Context參數或者Portlet 初始化參數,可以對Spring的DispatcherPortlet進行自定義。 下面列出了幾種可能。
Table 16.2. DispatcherPortlet的初始化參數
參數 | 解釋 |
---|---|
contextClass | 實現WebApplicationContext 的類,在Portlet初始化時用它初始化context。如果沒有指定這個 參數,會使用XmlPortletApplicationContext。 |
contextConfigLocation | 傳給context實例(由contextClass指定) 的字符串,指明context的位置。它可以(以逗號)分隔為多個字符串來 支持多個context(在定義過兩次的bean有多個context位置時, 最后的位置起作用)。 |
namespace | WebApplicationContext 的命名空間,缺省是[portlet-name]-portlet。 |
viewRendererUrl | ViewRendererServlet的URL, DispatcherPortlet可以訪問。 (見 Section 16.3, “ViewRendererServlet”)。 |
Portlet MVC中的顯示過程比Web MVC的復雜一點,為了復用所有Spring Web MVC里 的視圖技術,必須把 PortletRequest / PortletResponse 轉換到 HttpServletRequest / HttpServletResponse,然后調用 View的 render方法。為此,DispatcherPortlet 使用了一個特殊的servlet:ViewRendererServlet。
為了DispatcherPortlet能夠顯示, 必須在web.xml文件里為你的web應用聲明一個 ViewRendererServlet的實例,如下:
<servlet> <servlet-name>ViewRendererServlet</servlet-name> <servlet-class>org.springframework.web.servlet.ViewRendererServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ViewRendererServlet</servlet-name> <url-pattern>/WEB-INF/servlet/view</url-pattern> </servlet-mapping>
在實際執行顯示時,DispatcherPortlet這樣做:
-
把 WebApplicationContext作為屬性綁定在請求上, 使用和DispatcherServlet相同的 WEB_APPLICATION_CONTEXT_ATTRIBUTEkey。
-
把Model和 View對象綁定在請求上,使它們對 ViewRendererServlet可見。
-
構造 PortletRequestDispatcher對象,利用 映射到ViewRendererServlet的/WEB- INF/servlet/viewURL來執行include操作。
然后,ViewRendererServlet能夠以合適的參數 調用View的render方法。
可以通過DispatcherPortlet的viewRendererUrl 配置參數來修改ViewRendererServlet的實際URL。
Portlet MVC里的控制器和Web MVC的很想相似,在兩者之間移植代碼應該很簡單。
Portlet MVC控制器構架的基礎是 org.springframework.web.portlet.mvc.Controller 接口,如下所示。
public interface Controller { /** * Process the render request and return a ModelAndView object which the * DispatcherPortlet will render. */ ModelAndView handleRenderRequest(RenderRequest request, RenderResponse response) throws Exception; /** * Process the action request. There is nothing to return. */ void handleActionRequest(ActionRequest request, ActionResponse response) throws Exception; }
如你所見,Portlet Controller接口需要兩個方法來處理Portlet 請求的兩個階段:動作請求和顯示請求。動作階段應該能夠處理動作請求,顯示階段應該 能夠處理顯示請求,並返回合適的模型和視圖。 盡管Controller接口是抽象的,但Spring Portlet MVC 提供了很多包含了各種各樣你需要的功能的控制器-它們中的大多數和Spring Web MVC里的控制器很類似。 Controller接口只定義每個控制器需要的通用的功能 - 處理動作請求,處理顯示請求,返回模型和視圖。
當然,僅一個Controller 是不夠的。為了提供基本的功能,所有的Spring Portlet Controller從 AbstractController繼承,后者可以訪問Spring 的ApplicationContext和控制緩存。
Table 16.3. AbstractController提供的功能
參數 | 解釋 |
---|---|
requireSession | 表明當前的 Controller是否需要session。 所有的控制器都能使用這個功能。如果這樣的控制器收到請求時, session不存在,用戶會收到 SessionRequiredException。 |
synchronizeSession | 如果需要控制器在處理用戶session時保持同步,使用 這個參數。更具體來說,擴展的控制器會覆蓋handleRenderRequestInternal(..) 和handleActionRequestInternal(..)方法,如果指定了這個參數, 這兩個方法會在處理用戶session時保持同步。 |
renderWhenMinimized | 如果需要在portlet最小化狀態時,控制器也顯示視圖, 把這個參數設為true。這個參數缺省是false,所以portlet在最小化狀態 時,不顯示內容。 |
cacheSeconds | 在需要控制器覆蓋當前portlet定義的缺省緩存失效時間時, 設置一個正的整數。這個參數缺省是-1, 表示不改變缺省的緩存,把它設為0,就是 確保不緩存結果。 |
requireSession和 cacheSeconds屬性是在 AbstractController的父類 PortletContentGenerator里聲明的。為了完整性, 把它們列在這里。
在你自己的控制器里繼承AbstractController時 (不推薦這樣做,因為已經有許多現成的控制器,它們可能有你需要的功能),僅需要覆蓋 handleActionRequestInternal(ActionRequest, ActionResponse)方法或 handleRenderRequestInternal(RenderRequest, RenderResponse)方法(或兩者都覆蓋),實現邏輯, 並返回 ModelAndView 對象 (如果是 handleRenderRequestInternal方法)。
handleActionRequestInternal(..)和 handleRenderRequestInternal(..)方法的缺省實現都會 拋出 PortletException,這和JSR-168規范API里的 GenericPortlet的行為是一致的。所以只要覆蓋你的控制器 需要處理的方法。
下面簡短的例子包含了一個類和一個在web應用context里的聲明。
package samples; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import org.springframework.web.portlet.mvc.AbstractController; import org.springframework.web.portlet.ModelAndView; public class SampleController extends AbstractController { public ModelAndView handleRenderRequestInternal( RenderRequest request, RenderResponse response) throws Exception { ModelAndView mav = new ModelAndView("foo"); mav.addObject("message", "Hello World!"); return mav; } } <bean id="sampleController" class="samples.SampleController"> <property name="cacheSeconds" value="120"/> </bean>
為了使得一個簡單的控制器工作,你只需要類似上面的類和在web應用context里的聲明, 並且再設置一下處理器映射 (見 Section 16.5, “處理器映射”)。
盡管你能夠繼承AbstractController, Spring Portlet MVC提供了不少具體的實現,它們提供了許多在簡單MVC應用里 常用的功能。
ParameterizableViewController基本上 和上面的例子類似,除了你能指定web應用context返回的視圖的名字。 (不需要寫死視圖名)。
PortletModeNameViewController把當前的 Portlet的狀態作為視圖名,如果Portlet在View模式 (比如:PortletMode.VIEW),那“View”就是視圖名。
Spring Portlet MVC提供了和Spring Web MVC完全一致的 command controllers層次結構,提供方法來與數據對象交互 並且動態地把參數從PortletRequest 綁定到數據對象上。數據對象不需要實現框架相關的接口,因而你可以 直接操作這些持久化對象。下面讓我們查看Command控制器提供的功能, 來了解它們的使用:
-
AbstractCommandController - Command控制器,可以用來創建自己的控制器,它能夠將請求里的參數 綁定到指定的數據對象。這個類不提供表單功能,但它提供驗證功能,並且 可以在控制器里指定如何處理帶有請求參數的Command對象.
-
AbstractFormController - 提供表單提交支持的抽象控制器。你能夠對表單進行建模,通過從控制器 里得到的Command對象來填充表單。在用戶提交表單后, AbstractFormController會綁定字段、進行驗證, 然后把對象返回給控制器來做下一步的動作。支持的功能有:無效表單提交(重新 提交)、驗正和通常的表單流程。你需要實現方法來決定表單的顯示和成功時使用的 視圖。如果你需要表單,但不想在應用context里指定用戶看到的視圖,使用這個 控制器。
-
SimpleFormController - 一個具體的AbstractFormController, 對使用對應的command對象生成表單提供了更多的支持。 SimpleFormController可以讓你在用戶成功地提交 表單或其它狀態時,指定command對象,表單的視圖名以及頁面對應的視圖名。
-
AbstractWizardFormController – 具體的AbstractFormController,它提交了向導式的接口 來編輯跨多個頁面的command對象。支持多種用戶動作:完成、取消或者頁面變化,所有這些 都可以簡便地在視圖的請求參數里指定。
這些command控制器是非常強大的,為了有效地使用,需要對它們的原理有 細致的理解。在你開始使用它們前,務必仔細閱讀它們層次結構的javadoc以及示例。
除了開發新的控制器,我們可以重用現有的portlet並且在 DispatcherPortlet 把請求映射指向它們。通過 PortletWrappingController,你能實例化一個 現有的Portlet來作 Controller,如下所示:
<bean id="wrappingController" class="org.springframework.web.portlet.mvc.PortletWrappingController"> <property name="portletClass" value="sample.MyPortlet"/> <property name="portletName" value="my-portlet"/> <property name="initParameters"> <value> config=/WEB-INF/my-portlet-config.xml </value> </property> </bean>
這會很有價值,因為可以使用攔截器來對送向這些portlet的請求進行預處理和后處理。 而且也很方便,因為JSR-168沒有提供對過濾機制的支持。比如,可以在一個MyFaces JSR Portlet外面加上Hibernate的 OpenSessionInViewInterceptor。
通過處理器映射,可以把進來的portlet請求對應到合適的處理器上。已經有一些 現成的處理器映射可以使用,比如PortletModeHandlerMapping。 但還是讓我們先看一下HandlerMapping的一般概念。
注意,我們這里有意使用“處理器”來代替“控制器”。 DispatcherPortlet是設計用來和多種方式一起處理請求的, 而不僅僅是和Spring Portlet MVC自己的控制器。處理器是任意可以處理Portlet請求的對象。 控制器當然缺省是一種處理器。要將DispatcherPortlet和一些其他的框架一起使用,只需要實現相應的HandlerAdapter就可以了。
HandlerMapping提供的基本功能是提供一個 HandlerExecutionChain,后者必須包含匹配進來請求的 的處理器,也可能包含需要應用到請求的處理器攔截器的列表。當一個請求進來時, DispatcherPortlet會把它交給處理器射映,讓它來檢查 請求並得到合適的HandlerExecutionChain。然后 DispatcherPortlet會執行處理器以及chain里的攔截器。這些 概念和Spring Web MVC里的完全一致。
可配置的處理器映射非常強大,它可以包含攔截器(在實際的處理前、后進行預處理或后處理 或兩者都執行)。可以通過自定義一個HandlerMapping來加入許多功能。 想像一下,一個自定義的處理器映射,它不僅可以根據指定的portlet模式來選擇處理器, 也可以根據請求相聯系的session里的指定狀態來選擇。
在Spring Web MVC里,處理器映射通常是基於URL的。因為在Portlet里確實沒有URL, 必須使用其它的機制來控制映射。最常見的兩個是portlet模式和請求參數, 但在portlet請求里的任何對象都可以用在自定義的處理器映射中。
余下的章節會介紹在Spring Portlet MVC里最常見的三種處理器射映, 它們都繼承AbstractHandlerMapping並且共享以下的屬性:
-
interceptors: 需要使用的攔截器列表。 HandlerInterceptor在 Section 16.5.4, “增加 HandlerInterceptor”有討論。
-
defaultHandler: 在找不到匹配的處理器時, 缺省的處理器。
-
order: Spring會按照order屬性值 (見org.springframework.core.Ordered接口) 對context里的所有處理器映射進行排序,並且應用第一個匹配的處理器。
-
lazyInitHandlers: 用來Lazy初始化單例 處理器(prototype處理器是始終lazy初始化的)。缺省值是false。這個屬性是在這三個 具體處理器里直接實現。
這是一個簡單的處理器映射,它是基於當前的portlet模式(比如:'view', 'edit', 'help'). 如下:
<bean id="portletModeHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeHandlerMapping"> <property name="portletModeMap"> <map> <entry key="view" value-ref="viewHandler"/> <entry key="edit" value-ref="editHandler"/> <entry key="help" value-ref="helpHandler"/> </map> </property> </bean>
如果需要在不改變portlet模式的情況下而在多個控制器間切換, 最簡單的方法是把一個請求參數作為key來控制映射。
ParameterHandlerMapping使用一個特定的請求參數來控制映射。 這個參數的缺省名是'action',可以通過'parameterName'屬性來改變。
這個映射的bean設置會是這樣:
<bean id="parameterHandlerMapping" class="org.springframework.web.portlet.handler.ParameterHandlerMapping"/> <property name="parameterMap"> <map> <entry key="add" value-ref="addItemHandler"/> <entry key="edit" value-ref="editItemHandler"/> <entry key="delete" value-ref="deleteItemHandler"/> </map> </property> </bean>
最強大的內置處理映射 PortletModeParameterHandlerMapping結合了前兩者的功能, 能夠在每種portlet模式下進行不同的切換。
同樣,參數的缺省名是"action",但可以通過parameterName來修改。
缺省情況下,同樣的參數值不能在兩個不同的portlet模式下使用, 因為如果portlet自己改變了portlet模式,那么請求在映射中將不在有效。 把allowDupParameters屬性設為true可以改變這種行為,但這種做法是不推薦的。
這個映射的bean設置會是這樣:
<bean id="portletModeParameterHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping"> <property name="portletModeParameterMap"> <map> <entry key="view"> <!-- 'view' portlet模式 --> <map> <entry key="add" value-ref="addItemHandler"/> <entry key="edit" value-ref="editItemHandler"/> <entry key="delete" value-ref="deleteItemHandler"/> </map> </entry> <entry key="edit"> <!-- 'edit' portlet模式 --> <map> <entry key="prefs" value-ref="prefsHandler"/> <entry key="resetPrefs" value-ref="resetPrefsHandler"/> </map> </entry> </map> </property> </bean>
這個映射可以在處理鏈中放在 PortletModeHandlerMapping前面,它可以為每個模式以及全局提供 缺省的映射。
Spring的處理器映射機制里有處理器攔截器的概念,在希望對於特定的請求 應用不同的功能時,它是非常有用。比如,檢查用戶名(principal)。同樣,Spring Portlet MVC以Web MVC相同的方式實現了這些概念。
在處理器映射里的攔截器必須實現org.springframework.web.portlet 里的HandlerInterceptor接口。 和servlet的版本一樣,這個接口定義了三個方法:一個在實際的處理器執行前被調用 (preHandle),一個在執行后被調用(postHandle) 還有一個是在請求完全結束時被調用(afterCompletion)。 這三個方法應該可以為各種前置和后置處理提供足夠的靈活。
preHandle返回一個布爾值。可以使用這個方法來中斷或者繼續執行鏈的處理。 當返回true時,處理執行鏈會繼續,當返回false時, DispatcherPortlet 假設這個攔截器已經處理請求(比如,顯示了合適的視圖)並且不需要繼續執行其它的 攔截器和在執行鏈中實際的處理器。
postHandle只會在RenderRequest 中被調用。ActionRequest和RenderRequest 都會調用preHandle和afterCompletion方法。 如果希望只在其中的一種請求中執行你的代碼,務必在處理前檢查請求的類型。
和servlet包類似,portlet包里也有一個HandlerInterceptor的具體實現 - HandlerInterceptorAdapter。這個類所有方法都是空的, 所以可以繼承它,實現一個或兩個你所需要的方法。
Portlet包也帶一個名為ParameterMappingInterceptor 的具體攔截器,它可以和ParameterHandlerMapping 以及PortletModeParameterHandlerMapping一起使用。 這個攔截器可以把用來控制映射的參數從ActionRequest 帶到隨后的RenderRequest,這能夠確保 RenderRequest映射到和ActionRequest相同的處理器。這些都是在 preHandle方法里完成的,所以在你的處理器里仍然可以改變決定 RenderRequest映射的參數值。
注意這個攔截器會調用ActionResponse 的setRenderParameter方法,這意味着在使用它的時候, 不能在處理器里調用sendRedirect。如果確實需要重定向, 可以手工地把映射參數向前傳,或者另寫一個攔截器來處理。
如上面提到的那樣,Spring Portle MVC直接重用所有Sprint Web MVC里的視圖技術。 不僅包含了不同的View實現,也包含了視圖解析器的實現。 需要更多相關信息,請參考Chapter 14, 集成視圖技術和Section 13.5, “視圖與視圖解析”。
以下是一些在View和ViewResolver 中值得提及的:
-
大多數的門戶希望portlet的顯示結果是HTML片斷,所以像 JSP/JSTL,Velocity,FreeMaker和XSLT是行得通的。但有時候視圖也可能在portlet 里返回其它類型的文檔。
-
在portlet里不存在HTTP的重定向(ActionResponse 的sendRedirect(..)不能在portal中使用)。所以在Portlet MVC中 RedirectView和'redirect:'前綴是 不工作的。
-
在Portlet MVC里可以使用'forward:'前綴。 但是,記住,在portlet里,當前URL是不確定的,這意味着不能使用相對URL來 訪問web應用的資源,必須使用絕對URL。
對於JSP開發,新的Spring Taglib和Spring表單taglib會以在Servlet視圖里相同的方式 在portlet視圖里工作。
Spring Portlet MVC和Web MVC一樣,也支持multipart來處理portlet中的文件上傳。 插件式的PortletMultipartResolver提供了對multipart的支持, 它在org.springframework.web.portlet.multipart包里。 Spring提供了PortletMultipartResolver來和 Commons FileUpload一起使用。余下的篇幅會介紹文件上傳的支持。
缺省情況下,Spring Portlet是不會處理multipart的,如果開發人員需要處理multipart, 就必須在web應用的context里添加一個multipart解析器,然后, DispatcherPortlet會在每個請求里檢查是否帶有multipart。 如果沒找到,請求會繼續,如果找到了multipart,在context中聲明的 PortletMultipartResolver會被調用。接着, 在請求里的multipart屬性會和其它的屬性一樣被處理。
下面的例子介紹了 CommonsPortletMultipartResolver的使用:
<bean id="portletMultipartResolver"
class="org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver">
<!-- 一個屬性;以byte為單位的最大文件長度 -->
<property name="maxUploadSize" value="100000"/>
</bean>
當然為了使multipart解析器能夠工作,必須把合適的jar放到類路徑里。對於 CommonsMultipartResolver來說,需要 commons-fileupload.jar。注意,必須使用至少1.1 版本的Commons FileUpload,因為以前的版本不支持JSR-168應用。
現在你已經看到如何設置Portlet MVC來處理multipart請求,接下來我們 討論它的使用。當DispatcherPortlet檢測到 multipart時,它會激活在context里聲明的解析器,並把請求交給它。然后解析器 把當前的ActionRequest放到支持文件上傳的MultipartActionRequest中。通過 MultipartActionRequest,可以得到 請求包含的multipart信息,並且在控制器里訪問multipart文件。
注意,不能從RenderRequest接收到multipart 文件,而只能從ActionRequest里。
在 PortletMultipartResolver處理完后, 請求會繼續被處理。你需要創建一個帶有上傳字段的表單來使用它(見下面),Spring會 把文件綁定在你的表單上(支持對象)。為了讓用戶上傳文件,必須創建一個 (JSP/HTML)的表單:
<h1>Please upload a file</h1> <form method="post" action="<portlet:actionURL/>" enctype="multipart/form-data"> <input type="file" name="file"/> <input type="submit"/> </form>
如你所見,我們在bean的屬性后面創建名為“File”的字段 用來容納byte[]。加上了編碼屬性(enctype="multipart/form-data"), 讓瀏覽器知道怎樣來編碼multipart字段(不要忘記!)。
和其它那些不會自動轉化為字符串或原始類型的屬性一樣,為了把二進制數據放到對象 里,必須注冊一個使用PortletRequestDataBinder 的自定義的編輯器。現成有好幾個編輯器可以用來處理文件並把結果放到對象上。 StringMultipartFileEditor能夠把文件轉換成字符串 (使用用戶定義的字符集),ByteArrayMultipartFileEditor 能夠把文件轉換成字節數據。他們的功能和 CustomDateEditor一樣。
所以,為了能夠使用表單來上傳文件,需要聲明解析器,映射到處理這個bean的控制器的 映射以及控制器。
<bean id="portletMultipartResolver" class="org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver"/> <bean id="portletModeHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeHandlerMapping"> <property name="portletModeMap"> <map> <entry key="view" value-ref="fileUploadController"/> </map> </property> </bean> <bean id="fileUploadController" class="examples.FileUploadController"> <property name="commandClass" value="examples.FileUploadBean"/> <property name="formView" value="fileuploadform"/> <property name="successView" value="confirmation"/> </bean>
接着,創建控制器以及實際容納這個文件屬性的類。
public class FileUploadController extends SimpleFormController { public void onSubmitAction( ActionRequest request, ActionResponse response, Object command, BindException errors) throws Exception { // 類型轉換bean FileUploadBean bean = (FileUploadBean) command; // 是否有內容 byte[] file = bean.getFile(); if (file == null) { // 奇怪,用戶什么都沒有上傳 } // do something with the file here } protected void initBinder( PortletRequest request, PortletRequestDataBinder binder) throws Exception { // to actually be able to convert Multipart instance to byte[] // we have to register a custom editor binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor()); // 現在Spring知道如何來處理和轉換multipart對象 } } public class FileUploadBean { private byte[] file; public void setFile(byte[] file) { this.file = file; } public byte[] getFile() { return file; } }
如你所見,FileUploadBean有一個類型是 byte[]的屬性來容納文件。控制器注冊了一個自定義編輯器來 讓Spring知道如何把解析器發現的multipart轉換成指定的屬性。在這個例子里, 沒有對bean的byte[]屬性進行任何操作,但實際上,你可以做任 何操作(把它存到數據庫里,把它電郵出去,或其它)
下面是一個例子,文件直接綁定在的一個(表單支持)對象上的字符串類型屬性上面:
public class FileUploadController extends SimpleFormController { public void onSubmitAction( ActionRequest request, ActionResponse response, Object command, BindException errors) throws Exception { // cast the bean FileUploadBean bean = (FileUploadBean) command; // let's see if there's content there String file = bean.getFile(); if (file == null) { // hmm, that's strange, the user did not upload anything } // do something with the file here } protected void initBinder( PortletRequest request, PortletRequestDataBinder binder) throws Exception { // to actually be able to convert Multipart instance to a String // we have to register a custom editor binder.registerCustomEditor(String.class, new StringMultipartFileEditor()); // now Spring knows how to handle multipart objects and convert } } public class FileUploadBean { private String file; public void setFile(String file) { this.file = file; } public String getFile() { return file; } }
當然,最后的例子在上傳文本文件時才有(邏輯上的)意義(在上傳圖像文件時, 它不會工作)。
第三個(也是最后一個)選項是,什么情況下需要直接綁定在(表單支持)對象的 MultipartFile屬性上。在以下的情況, 不需要注冊自定義的屬性編輯器,因為不需要類型轉換。
public class FileUploadController extends SimpleFormController { public void onSubmitAction( ActionRequest request, ActionResponse response, Object command, BindException errors) throws Exception { // cast the bean FileUploadBean bean = (FileUploadBean) command; // let's see if there's content there MultipartFile file = bean.getFile(); if (file == null) { // hmm, that's strange, the user did not upload anything } // do something with the file here } } public class FileUploadBean { private MultipartFile file; public void setFile(MultipartFile file) { this.file = file; } public MultipartFile getFile() { return file; } }
和Web MVC一樣,Portlet MVC提供了 HandlerExceptionResolver來減輕處理 請求處理產生的意外異常時的痛苦。Portlet MVC同樣也提供了具體的 SimpleMappingExceptionResolver,可以將可能拋出的 異常對應到一個視圖名。
Spring Portlet MVC應用的部署過程和JSR-168 Portlet應用的一樣。然而, 這部分內容常常使人感到困惑,所以值得在這里簡單地介紹一下。
通常情況下,portal/portlet容器在servlet容器的某個Web應用中運行, 你的Portlet運行在servlet容器的另一個Web應用里。為了使得Portlet容器能夠調用 Portlet應用,Portlet容器必須對一個顯式的Servlet進行跨Context的調用,那個Servlet 提供了對在portlet.xml定義的Portlet服務的訪問支持。
JSR-168規范對這方面沒有規定,所以每個Portlet容器都有自己的機制,通常 會引入一些“布署時的處理”來改變Portlet應用並且把Portlet注冊到Portlet容器里。
至少,在Portlet應用中web.xml文件需要通過修改來注入 Portlet容器會顯式調用的Servlet。有時候,單個Servlet實例對Web應用中的所有 Portlet提供支持,有時候,對於每個Portlet需要一個Servlet實例。
有些Portlet容器也會在Web應用中注入類庫或者配置文件。Portlet容器需要 實現Portlet JSP Tab庫以供使用。
最重要的是理解你選擇的portal對Portlet布署的要求,並且確保滿足它們 (通常是按照它提供的自動布署程序)。仔細閱讀portal這方面的文檔。
在你布署完Portlet后,檢查web.xml。有些老的portal 會破壞ViewRendererServlet的定義,破壞你的Portlet 顯示。