Chapter 16. Portlet MVC框架

16.1. 介绍

Spring不仅支持传统(基于Servlet)的Web开发,也支持JSR-168 Portlet开发。 Portlet MVC框架尽可能多地采用Web MVC框架,使用相同的底层表现层抽象和整合技术。所以, 在继续阅读本章前,务必温习Chapter 13, Web框架Chapter 14, 集成视图技术两章。

[Note]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版本的 AbstractControllerhandleRequestInternal(..)方法,Portlet版本的 AbstractControllerhandleActionRequestInternal(..)handleRenderRequestInternal(..)方法。

这个框架是围绕着分发器 DispatcherPortlet设计的,分发器把请求转发给处理 器。和Web框架的DispatcherServlet一样, 这个框架还有可配置的处理器映射和视图解析,同时也支持文件上传。

Portlet MVC不支持本地化解析和主题解析 - 它们是portal/portlet容器 的范畴,并不适合放在Spring框架里。但是,Spring里所有依赖本地化(比如消息的 国际化)仍旧可以工作,因为DispatcherPortlet在以 DispatcherServlet相同的方式暴露当前的本地化信息。

16.1.1. 控制器 - MVC中的C

缺省的处理器是一个非常简单的 Controller接口,它提供了两个方法:

  • void handleActionRequest(request,response)

  • ModelAndView handleRenderRequest(request,response)

这个框架包含了许多相同的控制器实现层次,比如, AbstractControllerSimpleFormController等。它在数据绑定、命令对象使用、 模型处理和视图解析等方面和Servlet框架相同。

16.1.2. 视图 - MVC中的V

这个框架利用了一个特殊的桥Servlet ViewRendererServlet来使用Servlet框架里的视图显示 功能,这样,Portlet请求就被转化为Servlet请求,Portlet视图能够以通常的 Servlet底层代码来显示。这意味着,在Portlet里仍能使用当前所有的显示方法, 如JSP、Velocity等。

16.1.3. Web作用范围的Bean

Spring Portlet MVC支持Web Bean,这些Bean的生命周期在于当前的HTTP请求 或HTTP Session(一般的和全局的)里,这不是 框架自身的特性,而是由使用的容器的 WebApplicationContext提供的。 Section 3.4.3, “其他作用域”详细地描述了这些Bean的作用范围。

[Tip]Tip
???

Spring发布包带有完整的Spring Portlet MVC示例, 这个应用演示了所有Spring Portlet MVC框架的功能和特色。

你可以在samples/petportal目录下找到这个'petportal'应用。

16.2.  DispatcherPortlet

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处理请求的完整过程:

  1. PortletRequest.getLocale()返回 的Locale绑定在请求上,这使得在处理请求时(如显示视图、准备数据等), 代码能够使用Locale。

  2. 如果在ActionRequest里 指定了分段解析器,框架会在请求里寻找分段,如果找到了, 会把它们包装在MultipartActionRequest 里,供在后续处理中使用。(关于分段处理的进一步信息见Section 16.7, “Multipart文件上传支持” )。

  3. 寻找合适的处理器。如果找到了,这个处理器关联的执行链 (前置处理器、后置处理器和控制器)会被按序执行来准备模型。

  4. 如果有模型返回,视图通过视图解析器进行显示,视图解析器是在 WebApplicationContext配置好的。如果没有模型 返回(可能由于预处理器或后处理器拦截了请求,比如安全原因),就不会有视图显示 因为有可能请求已经被处理了。

WebApplicationContext里 定义的异常处理解析器能够捕获在处理请求时可能抛出的异常,借助这些解析器, 我们可以对在捕获特定异常时的操作进行自定义。

通过在portlet.xml文件里增加Context参数或者Portlet 初始化参数,可以对Spring的DispatcherPortlet进行自定义。 下面列出了几种可能。

Table 16.2.  DispatcherPortlet的初始化参数

参数解释
contextClass实现WebApplicationContext 的类,在Portlet初始化时用它初始化context。如果没有指定这个 参数,会使用XmlPortletApplicationContext
contextConfigLocation传给context实例(由contextClass指定) 的字符串,指明context的位置。它可以(以逗号)分隔为多个字符串来 支持多个context(在定义过两次的bean有多个context位置时, 最后的位置起作用)。
namespaceWebApplicationContext 的命名空间,缺省是[portlet-name]-portlet
viewRendererUrlViewRendererServlet的URL, DispatcherPortlet可以访问。 (见 Section 16.3, “ViewRendererServlet”)。

16.3. ViewRendererServlet

Portlet MVC中的显示过程比Web MVC的复杂一点,为了复用所有Spring Web MVC里 的视图技术,必须把 PortletRequest / PortletResponse 转换到 HttpServletRequest / HttpServletResponse,然后调用 Viewrender方法。为此,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这样做:

  1. WebApplicationContext作为属性绑定在请求上, 使用和DispatcherServlet相同的 WEB_APPLICATION_CONTEXT_ATTRIBUTEkey。

  2. ModelView对象绑定在请求上,使它们对 ViewRendererServlet可见。

  3. 构造 PortletRequestDispatcher对象,利用 映射到ViewRendererServlet/WEB- INF/servlet/viewURL来执行include操作。

然后,ViewRendererServlet能够以合适的参数 调用Viewrender方法。

可以通过DispatcherPortletviewRendererUrl 配置参数来修改ViewRendererServlet的实际URL。

16.4. 控制器

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接口只定义每个控制器需要的通用的功能 - 处理动作请求,处理显示请求,返回模型和视图。

16.4.1.  AbstractControllerPortletContentGenerator

当然,仅一个Controller 是不够的。为了提供基本的功能,所有的Spring Portlet ControllerAbstractController继承,后者可以访问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,就是 确保不缓存结果。

requireSessioncacheSeconds属性是在 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, “处理器映射”)。

16.4.2. 其它简单的控制器

尽管你能够继承AbstractController, Spring Portlet MVC提供了不少具体的实现,它们提供了许多在简单MVC应用里 常用的功能。

ParameterizableViewController基本上 和上面的例子类似,除了你能指定web应用context返回的视图的名字。 (不需要写死视图名)。

PortletModeNameViewController把当前的 Portlet的状态作为视图名,如果Portlet在View模式 (比如:PortletMode.VIEW),那“View”就是视图名。

16.4.3. Command控制器

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以及示例。

16.4.4.  PortletWrappingController

除了开发新的控制器,我们可以重用现有的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

16.5. 处理器映射

通过处理器映射,可以把进来的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: 需要使用的拦截器列表。 HandlerInterceptorSection 16.5.4, “增加 HandlerInterceptor”有讨论。

  • defaultHandler: 在找不到匹配的处理器时, 缺省的处理器。

  • order: Spring会按照order属性值 (见org.springframework.core.Ordered接口) 对context里的所有处理器映射进行排序,并且应用第一个匹配的处理器。

  • lazyInitHandlers: 用来Lazy初始化单例 处理器(prototype处理器是始终lazy初始化的)。缺省值是false。这个属性是在这三个 具体处理器里直接实现。

16.5.1.  PortletModeHandlerMapping

这是一个简单的处理器映射,它是基于当前的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>

16.5.2.  ParameterHandlerMapping

如果需要在不改变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>

16.5.3.  PortletModeParameterHandlerMapping

最强大的内置处理映射 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前面,它可以为每个模式以及全局提供 缺省的映射。

16.5.4. 增加 HandlerInterceptor

Spring的处理器映射机制里有处理器拦截器的概念,在希望对于特定的请求 应用不同的功能时,它是非常有用。比如,检查用户名(principal)。同样,Spring Portlet MVC以Web MVC相同的方式实现了这些概念。

在处理器映射里的拦截器必须实现org.springframework.web.portlet 里的HandlerInterceptor接口。 和servlet的版本一样,这个接口定义了三个方法:一个在实际的处理器执行前被调用 (preHandle),一个在执行后被调用(postHandle) 还有一个是在请求完全结束时被调用(afterCompletion)。 这三个方法应该可以为各种前置和后置处理提供足够的灵活。

preHandle返回一个布尔值。可以使用这个方法来中断或者继续执行链的处理。 当返回true时,处理执行链会继续,当返回false时, DispatcherPortlet 假设这个拦截器已经处理请求(比如,显示了合适的视图)并且不需要继续执行其它的 拦截器和在执行链中实际的处理器。

postHandle只会在RenderRequest 中被调用。ActionRequestRenderRequest 都会调用preHandleafterCompletion方法。 如果希望只在其中的一种请求中执行你的代码,务必在处理前检查请求的类型。

16.5.5. HandlerInterceptorAdapter

和servlet包类似,portlet包里也有一个HandlerInterceptor的具体实现 - HandlerInterceptorAdapter。这个类所有方法都是空的, 所以可以继承它,实现一个或两个你所需要的方法。

16.5.6. ParameterMappingInterceptor

Portlet包也带一个名为ParameterMappingInterceptor 的具体拦截器,它可以和ParameterHandlerMapping 以及PortletModeParameterHandlerMapping一起使用。 这个拦截器可以把用来控制映射的参数从ActionRequest 带到随后的RenderRequest,这能够确保 RenderRequest映射到和 ActionRequest相同的处理器。这些都是在 preHandle方法里完成的,所以在你的处理器里仍然可以改变决定 RenderRequest映射的参数值。

注意这个拦截器会调用ActionResponsesetRenderParameter方法,这意味着在使用它的时候, 不能在处理器里调用sendRedirect。如果确实需要重定向, 可以手工地把映射参数向前传,或者另写一个拦截器来处理。

16.6. 视图和它们的解析

如上面提到的那样,Spring Portle MVC直接重用所有Sprint Web MVC里的视图技术。 不仅包含了不同的View实现,也包含了视图解析器的实现。 需要更多相关信息,请参考Chapter 14, 集成视图技术Section 13.5, “视图与视图解析”

以下是一些在ViewViewResolver 中值得提及的:

  • 大多数的门户希望portlet的显示结果是HTML片断,所以像 JSP/JSTL,Velocity,FreeMaker和XSLT是行得通的。但有时候视图也可能在portlet 里返回其它类型的文档。

  • 在portlet里不存在HTTP的重定向(ActionResponsesendRedirect(..)不能在portal中使用)。所以在Portlet MVC中 RedirectView'redirect:'前缀是 工作的。

  • 在Portlet MVC里可以使用'forward:'前缀。 但是,记住,在portlet里,当前URL是不确定的,这意味着不能使用相对URL来 访问web应用的资源,必须使用绝对URL。

对于JSP开发,新的Spring Taglib和Spring表单taglib会以在Servlet视图里相同的方式 在portlet视图里工作。

16.7. Multipart文件上传支持

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属性会和其它的属性一样被处理。

16.7.1. 使用PortletMultipartResolver

下面的例子介绍了 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里。

16.7.2. 处理表单里的文件上传

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;
    }
}

16.8. 异常处理

和Web MVC一样,Portlet MVC提供了 HandlerExceptionResolver来减轻处理 请求处理产生的意外异常时的痛苦。Portlet MVC同样也提供了具体的 SimpleMappingExceptionResolver,可以将可能抛出的 异常对应到一个视图名。

16.9. Portlet应用的部署

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