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 显示。