`
yuexiaowen
  • 浏览: 121285 次
  • 性别: Icon_minigender_2
  • 来自: 咸阳
社区版块
存档分类
最新评论

TimerFactoryBean来建立tasks

 
阅读更多

第 12 章 Web框架

12.1. Web框架介绍

Spring的web框架是围绕分发器(DispatcherServlet)设计的,DispatcherServlet将请求分发到不同的处理器,框架还包括可配置的处理器映射,视图解析,本地化,主题解析,还支持文件上传。缺省的处理器是一个简单的控制器(Controller)接口,这个接口仅仅定义了ModelAndView handleRequest(request,response)方法。你可以实现这个接口生成应用的控制器,但是使用Spring提供的一系列控制器实现会更好一些,比如AbstractControllerAbstractCommandController,和SimpleFormController。应用控制器一般都从它们继承。注意你需要选择正确的基类:如果你没有表单,你就不需要一个FormController。这是和Structs的一个主要区别。

你可以使用任何对象作为命令对象或表单对象:不必实现某个接口或从某个基类继承。Spring的数据绑定相当灵活,例如,它认为类型不匹配这样的错误应该是应用级的验证错误,而不是系统错误。所以你不需要为了处理无效的表单提交,或者正确地转换字符串,在你的表单对象中用字符串类型重复定义你的业务对象属性。你应该直接绑定表单到业务对象上。这是和Struts的另一个重要不同,Struts是围绕象ActionActionForm这样的基类构建的,每一种行为都是它们的子类。

和WebWork相比,Spring将对象细分成不同的角色:它支持的概念有控制器(Controller),可选的命令对象(Command Object)或表单对象(Form Object),以及传递到视图的模型(Model)。模型不仅包含命令对象或表单对象,而且也包含任何引用数据。但是,WebWork的Action将所有的这些角色都合并在一个单独的对象里。WebWork允许你在表单中使用现有的业务对象,但是只能把它们定义成不同Action类的bean属性。更重要的是,在运算和表单赋值时,使用的是同一个处理请求的Action实例。因此,引用数据也需要被定义成Action的bean属性。这样在一个对象就承担了太多的角色。

对于视图:Spring的视图解析相当灵活。一个控制器实现甚至可以直接输出一个视图作为响应,这需要使用null返回ModelAndView。在一般的情况下,一个ModelAndView实例包含视图名字和模型映射表,模型映射表提供了bean的名字及其对象(比如命令对象或表单对象,引用数据等等)的对应关系。视图名解析的配置是非常灵活的,可以通过bean的名字,属性文件或者你自己的ViewResolver来实现。抽象的模型映射表完全抽象了表现层,没有任何限制:JSP,Velocity,或者其它的技术——任何表现层都可以直接和Spring集成。模型映射表仅仅将数据转换成合适的格式,比如JSP请求属性或者Velocity模版模型。

12.1.1. MVC实现的可扩展性

许多团队努力争取在技术和工具方面能使他们的投入更有价值,无论是现有的项目还是新的项目都是这样。具体地说,Struts 不仅有大量的书籍和工具,而且有许多开发者熟悉它。因此,如果你能忍受Struts的架构性缺陷,它仍然是web层一个很好的选择。WebWork和其它web框架也是这样。

如果你不想使用Spring的web MVC框架,而仅仅想使用Spring提供的其它功能,你可以很容易地将你选择的web框架和Spring结合起来。只要通过Spring的ContextLoadListener启动一个Spring的根应用上下文,并且通过它的ServletContext属性(或者Spring的各种帮助方法)在Struts或WebWork的Action中访问。注意到现在没有提到任何具体的“plugins”,因此这里也没有提及如何集成:从web层的角度看,你可以仅仅把Spring作为一个库使用,根应用上下文实例作为入口。

所有你注册的bean和Spring的服务可以在没有Spring的web MVC下被访问。Spring并没有在使用方法上和Struts或WebWork竞争,它只是提供单一web框架所没有的功能,从bean的配置到数据访问和事务处理。所以你可以使用Spring的中间层和(或者)数据访问层来增强你的应用,即使你只是使用象JDBC或Hibernate事务抽象这样的功能。

12.1.2. Spring MVC框架的特点

如果仅仅关注于web方面的支持,Spring有下面一些特点:

  • 清晰的角色划分:控制器,验证器,命令对象,表单对象和模型对象;分发器,处理器映射和视图解析器;等等。

  • 直接将框架类和应用类都作为JavaBean配置,包括通过应用上下文配置中间层引用,例如,从web控制器到业务对象和验证器的引用。

  • 可适应性,但不具有强制性:根据不同的情况,使用任何你需要的控制器子类(普通控制器,命令,表单,向导,多个行为,或者自定义的),而不是要求任何东西都要从Action/ActionForm继承。

  • 可重用的业务代码,而不需要代码重复:你可以使用现有的业务对象作为命令对象或表单对象,而不需要在ActionForm的子类中重复它们的定义。

  • 可定制的绑定和验证:将类型不匹配作为应用级的验证错误,这可以保存错误的值,以及本地化的日期和数字绑定等,而不是只能使用字符串表单对象,手动解析它并转换到业务对象。

  • 可定制的处理器映射,可定制的视图解析:灵活的模型可以根据名字/值映射,处理器映射和视图解析使应用策略从简单过渡到复杂,而不是只有一种单一的方法。

  • 可定制的本地化和主题解析,支持JSP,无论有没有使用Spring标签库,支持JSTL,支持不需要额外过渡的Velocity,等等。

  • 简单而强大的标签库,它尽可能地避免在HTML生成时的开销,提供在标记方面的最大灵活性。

12.2. 分发器(DispatcherServlet

Spring的web框架——象其它web框架一样——是一个请求驱动的web框架,其设计围绕一个能将请求分发到控制器的servlet,它也提供其它功能帮助web应用开发。然而,Spring的DispatcherServlet所做的不仅仅是这些。它和Spring的ApplicationContext完全集成在一起,允许你使用Spring的其它功能。

DispatcherServlet和其它servlet一样定义在你的web应用的web.xml文件里。DispatcherServlet处理的请求必须在同一个web.xml文件里使用url-mapping定义映射。

<web-app>
    ...
    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>    
</web-app>

在上面的例子里,所有以.form结尾的请求都会由DispatcherServlet处理。接下来需要配置DispatcherServlet本身。正如在第 3.10 节 “介绍ApplicationContext”中所描述的,Spring中的ApplicationContexts可以被限制在不同的作用域。在web框架中,每个DispatcherServlet有它自己的WebApplicationContext,它包含了DispatcherServlet配置所需要的bean。DispatcherServlet 使用的缺省BeanFactory是XmlBeanFactory,并且DispatcherServlet在初始化时会在你的web应用的WEB-INF目录下寻找[servlet-name]-servlet.xml文件。DispatcherServlet使用的缺省值可以使用servlet初始化参数修改(详细信息如下)。

WebApplicationContext仅仅是一个拥有web应用必要功能的普通ApplicationContext。它和一个标准的ApplicationContext的不同之处在于它能够解析主题(参考第 12.7 节 “主题使用”),并且它知道和那个servlet关联(通过到ServletContext的连接)。WebApplicationContext被绑定在ServletContext上,当你需要的时候,可以使用RequestContextUtils找到WebApplicationContext。

Spring的DispatcherServlet有一组特殊的bean,用来处理请求和显示相应的视图。这些bean包含在Spring的框架里,(可选择)可以在WebApplicationContext中配置,配置方式就象配置其它bean的方式一样。这些bean中的每一个都在下面被详细描述。待一会儿,我们就会提到它们,但这里仅仅是让你知道它们的存在以便我们继续讨论DispatcherServlet。对大多数bean,都提供了缺省值,所有你不必要担心它们的值。

表 12.1. WebApplicationContext中特殊的bean

名称 解释
处理器映射(handler mapping(s)) (第 12.4 节 “处理器映射”) 前处理器,后处理器和控制器的列表,它们在符合某种条件下才被执行(例如符合控制器指定的URL)
控制器(controller(s)) (第 12.3 节 “控制器”) 作为MVC三层一部分,提供具体功能(或者至少能够访问具体功能)的bean
视图解析器(view resolver) (第 12.5 节 “视图与视图解析”) 能够解析视图名,在DispatcherServlet解析视图时使用
本地化信息解析器(locale resolver) (第 12.6 节 “使用本地化信息”) 能够解析用户正在使用的本地化信息,以提供国际化视图
主题解析器(theme resolver) (第 12.7 节 “主题使用”) 能够解析你的web应用所使用的主题,比如,提供个性化的布局
multipart解析器 (第 12.8 节 “Spring对multipart(文件上传)的支持”) 提供HTML表单文件上传功能
处理器异常解析器(handlerexception resolver) (第 12.9 节 “处理异常”) 将异常对应到视图,或者实现某种复杂的异常处理代码

当DispatcherServlet被安装配置好,DispatcherServlet一接收到请求,处理就开始了。下面的列表描述了DispatcherServlet处理请求的全过程:

  1. 搜索WebApplicationContext,并将它绑定到请求的一个属性上,以便控制器和处理链上的其它处理器能使用WebApplicationContext。缺省它被绑定在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE这个关键字上

  2. 绑定本地化信息解析器到请求上,这样使得处理链上的处理器在处理请求(显示视图,准备数据等等)时能解析本地化信息。如果你不使用本地化信息解析器,它不会影响任何东西,忽略它就可以了

  3. 绑定主题解析器到请求上,使得视图决定使用哪个主题(如果你不需要主题,可以忽略它,解析器仅仅是绑定,如果你不使用它,不会影响任何东西)

  4. 如果multipart解析器被指定,请求会被检查是否使用了multipart,如果是,multipart解析器会被保存在MultipartHttpServletRequest中以便被处理链中的其它处理器使用(下面会讲到更多有关multipart处理的内容)

  5. 搜索合适的处理器。如果找到,执行和这个处理器相关的执行链(预处理器,后处理器,控制器),以便准备模型数据

  6. 如果模型数据被返还,就使用配置在WebApplicationContext中的视图解析器,显示视图,否则(可能是安全原因,预处理器或后处理器截取了请求),虽然请求能够提供必要的信息,但是视图也不会被显示。

在请求处理过程中抛出的异常可以被任何定义在WebApplicationContext中的异常解析器所获取。使用这些异常解析器,你可以在异常抛出时定义特定行为。

Spring的DispatcherServlet也支持返回Servlet API定义的last-modification-date,决定某个请求最后修改的日期很简单。DispatcherServlet会首先寻找一个合适的处理器映射,检查处理器是否实现了LastModified接口,如果是,将long getLastModified(request)的值返回给客户端。

你可以在web.xml文件中添加上下文参数或servlet初始化参数定制Spring的DispatcherServlet。下面是一些可能的参数。

表 12.2. DispatcherServlet初始化参数

参数 解释
contextClass 实现WebApplicationContext的类,当前的servlet用它来实例化上下文。如果这个参数没有指定,使用XmlWebApplicationContext
contextConfigLocation 传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。这个字符串可以被分成多个字符串(使用逗号作为分隔符)来支持多个上下文(在多上下文的情况下,被定义两次的bean中,后面一个优先)
namespace WebApplicationContext命名空间。缺省是[server-name]-servlet

12.3. 控制器

控制器的概念是MVC设计模式的一部分。控制器定义了应用的行为,至少能使用户访问到这些行为。控制器解释用户输入,并将其转换成合理的模型数据,从而可以进一步地由视图展示给用户。Spring以一种抽象的方式实现了控制器概念,这样使得不同类型的控制器可以被创建。Spring包含表单控制器,命令控制器,执行向导逻辑的控制器等等。

Spring控制器架构的基础是org.springframework.mvc.Controller接口。

public interface Controller {

    /**
     * Process the request and return a ModelAndView object which the DispatcherServlet
     * will render.
     */
    ModelAndView handleRequest(
        HttpServletRequest request, 
        HttpServletResponse response) 
    throws Exception;
}

你可以发现Controller接口仅仅声明了一个方法,它能够处理请求并返回合适的模型和视图。Spring MVC实现的基础就是这三个概念:ModelAndViewController。 因为Controller接口是完全抽象的,Spring提供了许多已经包含一定功能的控制器。控制器接口仅仅定义了每个控制器提供的共同功能:处理请求并返回一个模型和一个视图。

12.3.1. AbstractController 和 WebContentGenerator

当然,就一个控制器接口并不够。为了提供一套基础设施,所有的Spring控制器都从 AbstractController 继承,AbstractController 提供缓存和其它比如 mimetype 的设置的功能。

表 12.3. AbstractController提供的功能

功能 解释
supportedMethods 指定这个控制器应该接受什么样的请求方法。通常它被设置成GETPOST,但是你可以选择你想支持的方法。如果控制器不支持请求发送的方法,客户端会得到通知(ServletException
requiresSession 指定这个控制器是否需要会话。这个功能提供给所有控制器。如果控制器在没有会话的情况下接收到请求,用户被通知ServletException
synchronizeSession 如果你需要使控制器同步访问用户会话,使用这个参数。具体地说,继承的控制器要重载handleRequestInternal方法,如果你指定了这个变量,控制器就被同步化。
cacheSeconds 当你需要控制器在HTTP响应中生成缓存指令,用这参数指定一个大于零的整数。缺省它被设置为-1,所以就没有生成缓存指令
useExpiresHeader 指定你的控制器使用HTTP 1.0兼容的"Expires"。缺省为true,所以你可以不用修改它
useCacheHeader 指定你的控制器使用HTTP 1.0兼容的"Cache-Control"。缺省为true,所以你也可以不用修改它

最后的两个属性是WebContentGenerator定义的,WebContentGeneratorAbstractController的超类……

当使用AbstractController作为你的控制器基类时(一般推荐这样做,因为有许多预定义的控制器你可以选择),你只需要重载handleRequestInternal(HttpServletRequest, HttpServletResponse)这个方法,实现你自己的逻辑,并返回一个ModelAndView对象。下面这个简单例子包含一个类和在web应用上下文中的定义。

package samples;

public class SampleController extends AbstractController {

    public ModelAndView handleRequestInternal(
        HttpServletRequest request,
        HttpServletResponse response)
    throws Exception {
        ModelAndView mav = new ModelAndView("foo", new HashMap());
    }
}

 

<bean id="sampleController" class="samples.SampleController">
    <property name="cacheSeconds"><value>120</value</property>
</bean>

除了这个类和在web应用上下文中的定义,还需要设置处理器映射(参考第 12.4 节 “处理器映射”),这样这个简单的控制器就可以工作了。这个控制器将生成缓存指令告诉客户端缓存数据2分钟后再检查状态。这个控制器还返回了一个硬编码的视图名(不是很好)(详情参考第 12.5 节 “视图与视图解析”)。

12.3.2. 其它的简单控制器

除了AbstractController——虽然有许多其他控制器可以提供给你更多的功能,但是你还是可以直接继承AbstractController——有许多简单控制器,它们可以减轻开发简单MVC应用时的负担。ParameterizableViewController基本上和上面例子中的一样,但是你可以指定返回的视图名,视图名定义在web应用上下文中(不需要硬编码的视图名)

FileNameViewController检查URL并获取文件请求的文件名(http://www.springframework.org/index.html的文件名是index),把它作为视图名。仅此而已。

12.3.3. MultiActionController

Spring提供一个多动作控制器,使用它你可以将几个动作合并在一个控制器里,这样可以把功能组合在一起。多动作控制器存在在一个单独的包中——org.springframework.web.mvc.multiaction——它能够将请求映射到方法名,然后调用正确的方法。比如当你在一个控制器中有很多公共的功能,但是想多个入口到控制器使用不同的行为,使用多动作控制器就特别方便。

表 12.4. MultiActionController提供的功能

功能 解释
delegate MultiActionController有两种使用方式。第一种是继承MultiActionController,并在子类中指定由MethodNameResolver解析的方法(这种情况下不需要这个配置参数),第二种是你定义了一个代理对象,由它调用Resolver解析的方法。如果你是这种情况,你必须使用这个配置参数定义代理对象
methodNameResolver 由于某种原因,MultiActionController需要基于收到的请求解析它必须调用的方法。你可以使用这个配置参数定义一个解析器

一个多动作控制器的方法需要符合下列格式:

// actionName can be replaced by any methodname
ModelAndView actionName(HttpServletRequest, HttpServletResponse);

由于MultiActionController不能判断方法重载(overloading),所以方法重载是不允许的。此外,你可以定义exception handlers,它能够处理从你指定的方法中抛出的异常。包含异常处理的动作方法需要返回一个ModelAndView对象,就象其它动作方法一样,并符合下面的格式:

// anyMeaningfulName can be replaced by any methodname
ModelAndView anyMeaningfulName(HttpServletRequest, HttpServletResponse, ExceptionClass);

ExceptionClass可以是任何异常,只要它是java.lang.Exceptionjava.lang.RuntimeException的子类。

MethodNameResolver根据收到的请求解析方法名。有三种解析器可以供你选择,当然你可以自己实现解析器。

  • ParameterMethodNameResolver - 解析请求参数,并将它作为方法名(http://www.sf.net/index.view?testParam=testIt的请求就会调用testIt(HttpServletRequest,HttpServletResponse))。使用paramName配置参数可以调整所检查的参数

  • InternalPathMethodNameResolver - 从路径中获取文件名作为方法名(http://www.sf.net/testing.view的请求会调用testing(HttpServletRequest, HttpServletResponse)方法)

  • PropertiesMethodNameResolver - 使用用户定义的属性对象将请求的URL映射到方法名。当属性定义/index/welcome.html=doIt,并且收到/index/welcome.html的请求,就调用doIt(HttpServletRequest, HttpServletResponse)方法。这个方法名解析器需要使用PathMatcher(参考 第 12.10.1 节 “关于pathmatcher的小故事”)所以如果属性包含/**/welcom?.html,该方法也会被调用!

我们来看一组例子。首先是一个使用ParameterMethodNameResolver和代理属性的例子,它接受包含参数名的请求,调用方法retrieveIndex:

<bean id="paramResolver" class="org....mvc.multiaction.ParameterMethodNameResolver">
    <property name="paramName"><value>method</value></property>
</bean>

<bean id="paramMultiController" class="org....mvc.multiaction.MultiActionController">
    <property name="methodNameResolver"><ref bean="paramResolver"/></property>
    <property name="delegate"><ref bean="sampleDelegate"/>
</bean>

<bean id="sampleDelegate" class="samples.SampleDelegate"/>

## together with

public class SampleDelegate {

    public ModelAndView retrieveIndex(
        HttpServletRequest req, 
        HttpServletResponse resp) {

        rerurn new ModelAndView("index", "date", new Long(System.currentTimeMillis()));
    }
}

当使用上面的代理对象时,我们也可以使用PropertiesMethodNameRseolver来匹配一组URL,将它们映射到我们定义的方法上:

<bean id="propsResolver" class="org....mvc.multiaction.PropertiesMethodNameResolver">
    <property name="mappings">
        <props>
            <prop key="/index/welcome.html">retrieveIndex</prop>
            <prop key="/**/notwelcome.html">retrieveIndex</prop>
            <prop key="/*/user?.html">retrieveIndex</prop>
        </props>
    </property>
</bean>

<bean id="paramMultiController" class="org....mvc.multiaction.MultiActionController">
    <property name="methodNameResolver"><ref bean="propsResolver"/></property>
    <property name="delegate"><ref bean="sampleDelegate"/>
</bean>

12.3.4. 命令控制器

Spring的CommandControllers是Spring MVC包的重要部分。命令控制器提供了一种和数据对象交互的方式,并动态将来自HttpServletRequest的参数绑定到你指定的数据对象上。和Struts的actonform相比,在Spring中,你不需要实现任何接口来实现数据绑定。首先,让我们看一下有哪些可以使用的命令控制器,以便有一个清晰的了解:

  • AbstractCommandController - 你可以使用这个命令控制器来创建你自己的命令控制器,它能够将请求参数绑定到你指定的数据对象。这个类并不提供任何表单功能,但是它提供验证功能,并且让你在控制器中定义如何处理包含请求参数的数据对象。

  • AbstractFormController - 一个提供表单提交支持的控制器。使用这个控制器,你可以定义表单,并使用你从控制器获取的数据对象构建表单。当用户输入表单内容,AbstractFormController将用户输入的内容绑定到数据对象,验证这些内容,并将对象交给控制器,完成适当的动作。它所支持的功能有无效表单提交(再次提交),验证,和正确的表单工作流。你可以控制将什么视图绑定到你的AbstractFormController。如果你需要表单,但不想在应用上下文中指定显示给用户的视图,就使用这个控制器。

  • SimpleFormController - 这是一个更具体的FormCotnroller,它能用相应的数据对象帮助你创建表单。SimpleFormController让你指定一个命令对象,表单视图名,当表单提交成功后显示给用户的视图名等等。

  • WizardFormController - 最后一个也是功能最强的控制器。WizardFormController 允许你以向导风格处理数据对象,当使用大的数据对象时,这样的方式相当方便。

12.4. 处理器映射

使用处理器映射,你可以将web请求映射到正确的处理器上。有很多处理器映射你可以使用,例如:SimpleUrlHandlerMapping或者BeanNameUrlHandlerMapping,但是先让我们看一下HandlerMapping的基本概念。

一个基本的HandlerMapping所提供的功能是将请求传递到HandlerExecutionChain上,首先HandlerExecutionChain包含一个符合输入请求的处理器。其次(但是可选的)是一个可以拦截请求的拦截器列表。当收到请求,DispatcherServlet将请求交给处理器映射,让它检查请求并获得一个正确的HandlerExecutionChain。然后,执行定义在执行链中的处理器和拦截器(如果有拦截器的话)

包含拦截器(处理器执行前,执行后,或者执行前后)的可配置的处理器映射功能非常强大。许多功能被放置在自定义的HandlerMappings中。一个自定义的处理器映射不仅根据请求的URL,而且还可以根据和请求相关的会话状态来选择处理器。

我们来看看Spring提供的处理器映射。

12.4.1. BeanNameUrlHandlerMapping

BeanNameUrlHandlerMapping是一个简单但很强大的处理器映射,它将收到的HTTP请求映射到在web应用上下文中定义的bean的名字上。如果我们想要使用户插入一个账户,并且假设我们提供了FormController(关于CommandController和FormController请参考第 12.3.4 节 “命令控制器”)和显示表单的JSP视图(或Velocity模版)。当使用BeanNameUrlHandlerMapping时,我们用下面的配置能将包含URL http://samples.com/editaccount.form的HTTP请求映射到合适的FormController上:

<beans>
    <bean id="handlerMapping" 
          class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
        
    <bean name="/editaccount.form"
          class="org.springframework.web.servlet.mvc.SimpleFormController">
        <property name="formView"><value>account</value></property>
        <property name="successView"><value>account-created</value></property>
        <property name="commandName"><value>Account</value></property>
        <property name="commandClass"><value>samples.Account</value></property>
    </bean>
<beans>    

所有/editaccount.form的请求就会由上面的FormController处理。当然我们得在web.xml中定义servlet-mapping,接受所有以.form结尾的请求。

<web-app>
    ...
    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

   <!-- Maps the sample dispatcher to /*.form -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    ...
</web-app>

注意:如果你使用BeanNameUrlHandlerMapping,你不必在web应用上下文中定义它。缺省情况下,如果在上下文中没有找到处理器映射,DispatcherServlet会为你创建一个BeanNameUrlHandlerMapping

12.4.2. SimpleUrlHandlerMapping

另一个——更强大的处理器映射——是SimpleUrlHandlerMapping。它在应用上下文中可以配置,并且有Ant风格的路径匹配功能(参考第 12.10.1 节 “关于pathmatcher的小故事”)。下面几个例子可以帮助理解:

<web-app>
    ...
    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Maps the sample dispatcher to /*.form -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    ...
</web-app>

允许所有以.html和.form结尾的请求都由这个示例dispatchservelt处理。

<beans>
    <bean id="handlerMapping" 
          class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">            
        <property name="mappings">
            <props>
                <prop key="/*/account.form">editAccountFormController</prop>
                <prop key="/*/editaccount.form">editAccountFormController</prop>
                <prop key="/ex/view*.html">someViewController</prop>
                <prop key="/**/help.html">helpController</prop>
            </props>
        </property>
    </bean>
    
    <bean id="someViewController"
          class="org.springframework.web.servlet.mvc.FilenameViewController"/>
    
    <bean id="editAccountFormController"
          class="org.springframework.web.servlet.mvc.SimpleFormController">
        <property name="formView"><value>account</value></property>
        <property name="successView"><value>account-created</value></property>
        <property name="commandName"><value>Account</value></property>
        <property name="commandClass"><value>samples.Account</value></property>
    </bean>
<beans>

这个处理器映射首先将所有目录中文件名为help.html的请求传递给helpController(译注,原文为someViewController),someViewController是一个FilenameViewController(更多信息请参考第 12.3 节 “控制器”)。所有ex目录中资源名以view开始,.html结尾的请求都会被传递给控制器。这里定义了两个使用editAccountFormController的处理器映射。

12.4.3. 添加HandlerInterceptors

处理器映射提供了拦截器概念,当你想要为所有请求提供某种功能时,例如做某种检查,这就非常有用。

处理器映射中的拦截器必须实现org.springframework.web.servlet包中的HandlerInterceptor接口。这个接口定义了三个方法,一个在处理器执行被调用,一个在处理器执行被调用,另一个在整个请求处理完后调用。这三个方法提供你足够的灵活度做任何处理前和处理后的操作。

preHandle方法有一个boolean返回值。使用这个值,你可以调整执行链的行为。当返回true时,处理器执行链将继续执行,当返回false时,DispatcherServlet认为拦截器本身将处理请求(比如显示正确的视图),而不继续执行执行链中的其它拦截器和处理器。

下面的例子提供了一个拦截器,它拦截所有请求,如果当前时间是在上午9点到下午6点,将重定向到某个页面。

<beans>
    <bean id="handlerMapping" 
          class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">            
        <property name="interceptors">
            <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
        <property name="mappings">
            <props>
                <prop key="/*.form">editAccountFormController</prop>
                <prop key="/*.view">editAccountFormController</prop>
            </props>
        </property>
    </bean>
    
    <bean id="officeHoursInterceptor" 
          class="samples.TimeBasedAccessInterceptor">
        <property name="openingTime"><value>9</value></property>
        <property name="closingTime"><value>18</value></property>
    </bean>    
<beans>

 

package samples;

public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter {

    private int openingTime;    
    private int closingTime;    
    public void setOpeningTime(int openingTime) {
        this.openingTime = openingTime;
    }    
    public void setClosingTime(int closingTime) {
        this.closingTime = closingTime;
    }    
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler)
    throws Exception {
        Calendar cal = Calendar.getInstance();
        int hour = cal.get(HOUR_OF_DAY);
        if (openingTime <= hour < closingTime) {
            return true;
        } else {
            response.sendRedirect("http://host.com/outsideOfficeHours.html");
            return false;
        }
    }    
}

任何收到的请求,都将被TimeBasedAccessInterceptor截获,如果当前时间不在上班时间,用户会被重定向到一个静态html页面,比如告诉他只能在上班时间才能访问网站。

你可以发现,Spring提供了adapter,使你很容易地使用HandlerInterceptor

12.5. 视图与视图解析

所有web应用的MVC框架都会有它们处理视图的方式。Spring提供了视图解析器,这使得你在浏览器显示模型数据时不需要指定具体的视图技术。Spring允许你使用Java Server Page,Velocity模版和XSLT视图。第 13 章 集成表现层详细说明了如何集成不同的视图技术。

Spring处理视图的两个重要的类是ViewResolverViewView接口为请求作准备,并将请求传递给某个视图技术。ViewResolver提供了一个视图名和实际视图之间的映射。

12.5.1. ViewResolvers

正如前面所讨论的,SpringWeb框架的所有控制器都返回一个ModelAndView实例。Spring中的视图由视图名识别,视图解析器解析。Spring提供了许多视图解析器。我们将列出其中的一些,和它们的例子。

表 12.5. 视图解析器

ViewResolver 描述
AbstractCachingViewResolver 抽象视图解析器,负责缓存视图。许多视图需要在使用前作准备,从它继承的视图解析器可以缓存视图。
ResourceBundleViewResolver 使用ResourceBundle中的bean定义实现ViewResolver,这个ResourceBundle由bundle的basename指定。这个bundle通常定义在一个位于classpath中的一个属性文件中
UrlBasedViewResolver 这个ViewResolver实现允许将符号视图名直接解析到URL上,而不需要显式的映射定义。如果你的视图名直接符合视图资源的名字而不需要任意的映射,就可以使用这个解析器
InternalResourceViewResolver UrlBasedViewResolver的子类,它很方便地支持InternalResourceView(也就是Servlet和JSP),以及JstlView和TilesView的子类。由这个解析器生成的视图的类都可以通过setViewClass指定。详细参考UrlBasedViewResolver的javadocs
VelocityViewResolver UrlBasedViewResolver的子类,它能方便地支持VelocityView(也就是Velocity模版)以及它的子类

例如,当使用JSP时,可以使用UrlBasedViewResolver。这个视图解析器将视图名翻译成URL,并将请求传递给RequestDispatcher显示视图。

<bean id="viewResolver" 
      class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="prefix"><value>/WEB-INF/jsp/</value></property>
    <property name="suffix"><value>.jsp</value></property>
</bean>

当返回test作为视图名时,这个视图解析器将请求传递给RequestDispatcher,RequestDispatcher将请求再传递给/WEB-INF/jsp/test.jsp

当在一个web应用中混合使用不同的视图技术时,你可以使用ResourceBundleViewResolver:

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="baseName"><value>views</value></property>
    <property name="defaultParentView"><value>parentView</value></property
</bean>

12.6. 使用本地化信息

Spring架构的绝大部分都支持国际化,就象Spring的web框架一样。SpringWeb框架允许你使用客户端本地化信息自动解析消息。这由LocaleResolver对象完成。

当收到请求时,DispatcherServlet寻找一个本地化信息解析器,如果找到它就使用它设置本地化信息。使用RequestContext.getLocale()方法,你总可以获取本地化信息供本地化信息解析器使用。

除了自动本地化信息解析,你还可以将一个拦截器放置到处理器映射上(参考第 12.4.3 节 “添加HandlerInterceptors”),以便在某种环境下,比如基于请求中的参数,改变本地化信息。

本地化信息解析器和拦截器都定义在org.springframework.web.servlet.i18n包中,并且在你的应用上下文中配置。你可以选择使用Spring中的本地化信息解析器。

12.6.1. AcceptHeaderLocaleResolver

这个本地化信息解析器检查请求中客户端浏览器发送的accept-language头。通常这个头信息包含客户端操作系统的本地化信息。

12.6.2. CookieLocaleResolver

这个本地化信息解析器检查客户端中的cookie是否本地化信息被指定了。如果指定就使用该本地化信息。使用这个本地化信息解析器的属性,你可以指定cookie名,以及最大生存期。

<bean id="localeResolver">
    <property name="cookieName"><value>clientlanguage</value></property>
    <!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
    <property name="cookieMaxAge"><value>100000</value></property>
</bean>

这个例子定义了一个CookieLocaleResolver。

表 12.6. WebApplicationContext中的特殊bean

属性 缺省值 描述
cookieName classname + LOCALE cookie名
cookieMaxAge Integer.MAX_INT cookie在客户端存在的最大时间。如果该值是-1,这个cookie一直存在,直到客户关闭它的浏览器
cookiePath / 使用这个参数,你可以限制cookie只有你的一部分网站页面可以访问。当cookiePath被指定,cookie只能被该目录以及子目录的页面访问

12.6.3. SessionLocaleResolver

SessionLocaleResolver允许你从用户请求相关的会话中获取本地化信息。

12.6.4. LocaleChangeInterceptor

你可以使用LocaleChangeInterceptor修改本地化信息。这个拦截器需要添加到处理器映射中(参考第 12.4 节 “处理器映射”),并且它会在请求中检查参数修改本地化信息(它在上下文中的LocaleResolver中调用setLocale())。

<bean id="localeChangeInterceptor" 
      class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName"><value>siteLanguage</value></property>
</bean>
        
<bean id="localeResolver"
      class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
         
<bean id="urlMapping" 
      class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> 
    <property name="interceptors">
        <list>
            <ref local="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <props>
            <prop key="/**/*.view">someController</prop>
        </props>
    </property>
</bean>

所有包含参数siteLanguage的*.view资源的请求都会改变本地化信息。所以http://www.sf.net/home.view?siteLanguage=nl的请求会将网站语言修改为荷兰语。

12.7. 主题使用

空段落

12.8. Spring对multipart(文件上传)的支持

12.8.1. 介绍

Spring由内置的multipart支持web应用中的文件上传。multipart支持的设计是通过定义org.springframework.web.multipart包中的插件对象MultipartResovler来完成的。Spring提供MultipartResolver可以支持Commons FileUpload (http://jakarta.apache.org/commons/fileupload)和COS FileUpload (http://www.servlets.com/cos)。本章后面的部分描述了文件上传是如何支持的。

缺省,Spring是没有multipart处理,因为一些开发者想要自己处理它们。如果你想使用Spring的multipart,需要在web应用的上下文中添加multipart解析器。这样,每个请求就会被检查是否包含multipart。然而,如果请求中包含multipart,你的上下文中定义的MultipartResolver就会解析它。这样,你请求中的multipart属性就会象其它属性一样被处理。

12.8.2. 使用MultipartResolver

下面的例子说明了如何使用CommonsMultipartResolver

<bean id="multipartResolver" 
    class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

    <!-- one of the properties available; the maximum file size in bytes -->
    <property name="maximumFileSize">
        <value>100000</value>
    </property>
</bean>

这个例子使用CosMultipartResolver

<bean id="multipartResolver" 
    class="org.springframework.web.multipart.cos.CosMultipartResolver">

    <!-- one of the properties available; the maximum file size in bytes -->
    <property name="maximumFileSize">
        <value>100000</value>
    </property>
</bean>

当然你需要在你的classpath中为multipart解析器提供正确的jar文件。如果是CommonsMultipartResolver,你需要使用commons-fileupload.jar,如果是CosMultipartResolver,使用cos.jar

你已经看到如何设置Spring处理multipart请求,接下来我们看看如何使用它。当Spring的DispatchServlet发现multipart请求时,它会激活定义在上下文中的解析器并处理请求。它通常做的就是将当前的HttpServletRequest封装到支持multipart的MultipartHttpServletRequest。使用MultipartHttpServletRequest,你可以获取请求所包含的multipart信息,在控制器中获取具体的multipart内容。

12.8.3. 在一个表单中处理multipart

在MultipartResolver完成multipart解析后,multipart请求就会和其它请求一样被处理。使用multipart,你需要创建一个带文件上传域的表单,让Spring将文件绑定到你的表单上。就象其它不会自动转换成String或基本类型的属性一样,为了将二进制数据放到你的bean中,你必须用ServletRequestDatabinder注册一个自定义的编辑器。Spring有许多编辑器可以用来处理文件,以及在bean中设置结果。StringMultipartEditor能将文件转换成String(使用用户定义的字符集),ByteArrayMultipartEditor能将文件转换成字节数组。它们就象CustomDateEditor一样工作。

所以,为了在网站中使用表单上传文件,需要声明解析器,将URL映射到控制器,以及处理bean的控制器本身。

<beans>

    ...

    <bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/upload.form">fileUploadController</prop>
            </props>
        </property>
    </bean>

    <bean id="fileUploadController" class="examples.FileUploadController">
        <property name="commandClass"><value>examples.FileUploadBean</value></property>
        <property name="formView"><value>fileuploadform</value></property>
        <property name="successView"><value>confirmation</value></property>
    </bean>

</beans>

然后,创建控制器和含有文件属性的bean

// snippet from FileUploadController
public class FileUploadController extends SimpleFormController {

    protected ModelAndView onSubmit(
        HttpServletRequest request,
        HttpServletResponse response,
        Object command,
        BindException errors)
        throws ServletException, IOException {
        
        // cast the bean
        FileUploadBean bean = (FileUploadBean)command;
        
        // let's see if there's content there
        byte[] file = bean.getFile();
        if (file == null) {
            // hmm, that's strange, the user did not upload anything
        }

        // well, let's do nothing with the bean for now and return:        
        return super.onSubmit(request, response, command, errors);
    }
    
    protected void initBinder(
        HttpServletRequest request,
        ServletRequestDataBinder binder)
        throws ServletException {
        // to actually be able to convert Multipart instance to byte[]
        // we have to register a custom editor (in this case the
        // ByteArrayMultipartEditor
        binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor());
        // now Spring knows how to handle multipart object and convert them
    }
        
}

// snippet from FileUploadBean
public class FileUploadBean {
    private byte[] file;
    
    public void setFile(byte[] file) {
        this.file = file;
    }
    
    public byte[] getFile() {
        return file;
    }
}

你会看到,FileUploadBean有一个byte[]类型的属性来存放文件。控制器注册一个自定义的编辑器以便让Spring知道如何将解析器发现的multipart对象转换成bean指定的属性。在这些例子中,没有对bean的byte[]类型的属性做任何处理,但是在实际中可以做任何你想做的(将文件存储在数据库中,通过电子邮件发送给某人,等等)。

但是我们还没有结束。为了让用户能真正上传些东西,我们必须创建表单:

<html>
    <head>
        <title>Upload a file please</title>
    </head>
    <body>
        <h1>Please upload a file</h1>
        <form method="post" action="upload.form" enctype="multipart/form-data">
            <input type="file" name="file"/>
            <input type="submit"/>
        </form>
    </body>
</html>

你可以看到,我们在bean的byte[]类型的属性后面创建了一个域。我们还添加了编码属性以便让浏览器知道如何编码multipart的域(千万不要忘记!)现在就可以工作了。

12.9. 处理异常

Spring提供了HandlerExceptionResolvers来帮助处理控制器处理你的请求时所发生的异常。HandlerExceptionResolvers在某种程度上和你在web应用的web.xml中定义的异常映射很相象。然而,它们提供了一种更灵活的处理异常的方式。首先,HandlerExceptionResolver通知你当异常抛出时如何处理。并且,这种可编程的异常处理方式使得在请求被传递到另一个URL前给了你更多的响应选择。(这就和使用servlet特定异常映射的情况一样)。

实现HandlerExceptionResolver需要实现resolveException(Exception, Handler)方法并返回ModelAndView,除了HandlerExceptionResolver,你还可以使用SimpleMappingExceptionResolver。这个解析器使你能够获取任何抛出的异常的类名,并将它映射到视图名。这和servlet API的异常映射在功能上是等价的,但是它还为不同的处理器抛出的异常做更细粒度的映射提供可能。

12.10. 共同用到的工具

12.10.1. 关于pathmatcher的小故事

第 13 章 集成表现层

13.1. 简介

Spring之所以出色的一个原因就是将表现层从MVC的框架中分离出来。例如,通过配置就可以让Velocity或者XSLT来代替已经存在的JSP页面。本章介绍和Spring集成的一些主流表现层技术,并简要描述如何集成新的表现层。这里假设你已经熟悉第 12.5 节 “视图与视图解析”,那里介绍了将表现层集成到MVC框架中的基本知识。

13.2. 和JSP & JSTL一起使用Spring

Spring为JSP和JSTL提供了一组方案(顺便说一下,它们都是最流行的表现层技术之一)。使用JSP或JSTL需要使用定义在WebApplicationContext里的标准的视图解析器。此外,你当然也需要写一些JSP页面来显示页面。这里描述一些Spring为方便JSP开发而提供的额外功能。

13.2.1. 视图解析器

就象和Spring集成的其他表现层技术一样,对于JSP页面你需要一个视图解析器来解析。最常用的JSP视图解析器是InternalResourceViewResolverResourceBundleViewResolver。它们被定义在WebApplicationContext里:

# The ResourceBundleViewResolver:
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename"><value>views</value></property>
</bean>

# And a sample properties file is uses (views.properties in WEB-INF/classes):
welcome.class=org.springframework.web.servlet.view.JstlView
welcome.url=/WEB-INF/jsp/welcome.jsp

productList.class=org.springframework.web.servlet.view.JstlView
productList.url=/WEB-INF/jsp/productlist.jsp

你可以看到ResourceBundleViewResolver需要一个属性文件来把视图名称映射到 1)类和 2) URL。 通过ResolverBundleViewResolver,你可以用一个解析器来解析两种类型的视图。

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
    <property name="prefix"><value>/WEB-INF/jsp/</value></property>
    <property name="suffix"><value>.jsp</value></property>
</bean>

InternalResourceBundleViewResolver可以配置成使用JSP页面。作为好的实现方式,强烈推荐你将JSP文件放在WEB-INF下的一个目录中,这样客户端就不会直接访问到它们。

13.2.2. 普通JSP页面和JSTL

当你使用Java标准标签库(Java Standard Tag Library)时,你必须使用一个特殊的类,JstlView,因为JSTL在使用象I18N这样的功能前需要一些准备工作。

13.2.3. 其他有助于开发的标签

正如前面几章所提到的,Spring可以将请求参数绑定到命令对象上。为了更容易地开发含有数据绑定的JSP页面,Spring定义了一些专门的标签。所有的Spring标签都有HTML转义功能来决定是否使用字符转义。

标签库描述符(TLD)和库本身都包含在spring.jar里。更多有关标签的信息可以访问http://www.springframework.org/docs/taglib/index.html.

13.3. Tiles的使用

Tiles象其他表现层技术一样,可以集成在使用Spring的Web应用中。下面大致描述一下过程。

13.3.1. 所需的库文件

为了使用Tiles,你必须将需要的库文件包含在你的项目中。下面列出了这些库文件。

 

  • struts version 1.1

  • commons-beanutils

  • commons-digester

  • commons-logging

  • commons-lang

 

这些文件以从Spring中获得。

13.3.2. 如何集成Tiles

为了使用Tiles,你必须用定义文件(definition file)来配置它(有关于定义(definition)和其他Tiles概念,请参考http://jakarta.apache.org/struts)。在Spring中,这些都可以使用TilesConfigurer在完成。下面是ApplicationContext配置的片段。

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles.TilesConfigurer">
    <property name="factoryClass">
        <value>org.apache.struts.tiles.xmlDefinition.I18nFactorySet</value>
    </property>
    <property name="definitions">
        <list>
            <value>/WEB-INF/defs/general.xml</value>
            <value>/WEB-INF/defs/widgets.xml</value>
            <value>/WEB-INF/defs/administrator.xml</value>
            <value>/WEB-INF/defs/customer.xml</value>
            <value>/WEB-INF/defs/templates.xml</value>
        </list>
    </property>
</bean>

 

你可以看到,有五个文件包含定义,它们都存放在WEB-INF/defs目录中。当初始化WebApplicationContext时,这些文件被读取,并且初始化由factoryClass属性指定的定义工厂(definitons factory)。在这之后,你的Spring Web应用就可以使用在定义文件中的tiles includes内容。为了使用这些,你必须得和其他表现层技术一样有一个ViewResolver。有两种可以选择,InternalResourceViewResolverResourceBundleViewResolver

13.3.2.1.  InternalResourceViewResolver

InternalResourceViewResolver用viewClass属性指定的类实例化每个它解析的视图。

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="requestContextAttribute"><value>requestContext</value></property>
    <property name="viewClass">
        <value>org.springframework.web.servlet.view.tiles.TilesView</value>
    </property>
</bean>

 

13.3.2.2.  ResourceBundleViewResolver

必须提供给ResourceBundleViewResolver一个包含viewnames和viewclassess属性的属性文件。

<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename"><value>views</value></property>
</bean>

 

 

    ...
    welcomeView.class=org.springframework.web.servlet.view.tiles.TilesView
    welcomeView.url=welcome (<b>this is the name of a definition</b>)
        
    vetsView.class=org.springframework.web.servlet.view.tiles.TilesView
    vetsView.url=vetsView (<b>again, this is the name of a definition</b>)
        
    findOwnersForm.class=org.springframework.web.servlet.view.JstlView
    findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
    ...

你可以发现,当使用ResourceBundleViewResolver,你可以使用不同的表现层技术。

13.4. Velocity

Velocity是Jakarta项目开发的表现层技术。有关与Velocity的详细资料可以在http://jakarta.apache.org/velocity找到。这一部分介绍如何集成Velocity到Spring中。

13.4.1. 所需的库文件

在使用Velocity之前,你需要在你的Web应用中包含两个库文件,velocity-1.x.x.jarcommons-collections.jar 。一般它们放在WEB-INF/lib目录下,以保证J2EE服务器能够找到,同时把它们加到你的classpath中。当然假设你也已经把spring.jar放在你的WEB-INF/lib目录下!最新的Velocity和commnons collections的稳定版本由Spring框架提供,可以从/lib/velocity/lib/jakarta-commons目录下获取。

13.4.2. 分发器(Dispatcher Servlet)上下文

你的Spring DispatcherServlet配置文件(一般是WEB-INF/[servletname]-servlet.xml)应该包含一个视图解析器的bean定义。我们也可以再加一个bean来配置Velocity环境。我指定DispatcherServlet的名字为‘frontcontroller’,所以配置文件的名字反映了DispatcherServlet的名字

下面的示例代码显示了不同的配置文件

<!-- ===========================================================-->
<!-- View resolver. Required by web framework.                  -->
<!-- ===========================================================-->
<!--
  View resolvers can be configured with ResourceBundles or XML files.  If you need
  different view resolving based on Locale, you have to use the resource bundle resolver, 
  otherwise it makes no difference.  I simply prefer to keep the Spring configs and 
  contexts in XML.  See Spring documentation for more info.
-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
    <property name="cache"><value>true</value></property>
    <property name="location"><value>/WEB-INF/frontcontroller-views.xml</value></property>
</bean>

<!-- ===========================================================-->
<!-- Velocity configurer.                                       -->
<!-- ===========================================================-->
<!--
  The next bean sets up the Velocity environment for us based on a properties file, the 
  location of which is supplied here and set on the bean in the normal way.  My example shows
  that the bean will expect to find our velocity.properties file in the root of the 
  WEB-INF folder.  In fact, since this is the default location, it's not necessary for me
  to supply it here.  Another possibility would be to specify all of the velocity
  properties here in a property set called "velocityProperties" and dispense with the
  actual velocity.properties file altogether.
-->
<bean 
    id="velocityConfig" 
    class="org.springframework.web.servlet.view.velocity.VelocityConfigurer"
    singleton="true">
    <property name="configLocation"><value>/WEB-INF/velocity.properties</value></property>          
</bean>

13.4.3. Velocity.properties

这个属性文件用来配置Velocity,属性的值会传递给Velocity运行时。其中只有一些属性是必须的,其余大部分属性是可选的 - 详细可以查看Velocity的文档。这里,我仅仅演示在Spring的MVC框架下运行Velocity所必需的内容。

13.4.3.1. 模版位置

大部分属性值和Velocity模版的位置有关。Velocity模版可以通过classpath或文件系统载入,两种方式都有各自的优缺点。从classpath载入具有很好的移植性,可以在所有的目标服务器上工作,但你会发现在这种方式中,模版文件会把你的java包结构弄乱(除非你为模版建立独立树结构)。从classpath载入的另一个重要缺点是在开发过程中,在源文件目录中的任何改动常常会引起WEB-INF/classes下资源文件的刷新,这将导致开发服务器重启你的应用(代码的即时部署)。这可能是令人无法忍受的。一旦完成大部分的开发工作,你可以把模版文件存在在jar中,并把它放在WEB-INF/lib目录下中。

13.4.3.2. velocity.properties示例

这个例子将Velocity模版存放在文件系统的WEB-INF下,客户浏览器是无法直接访问到它们的,这样也不会因为你开发过程中修改它们而引起Web应用重启。它的缺点是目标服务器可能不能正确解析指向这些文件的路径,尤其是当目标服务器没有把WAR模块展开在文件系统中。Tomcat 4.1.x/5.x,WebSphere 4.x和WebSphere 5.x支持通过文件系统载入模版。但是你在其他类型的服务器上可能会有所不同。

#
# velocity.properties - example configuration
#


# uncomment the next two lines to load templates from the 
# classpath (i.e. WEB-INF/classes)
#resource.loader=class
#class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader

# comment the next two lines to stop loading templates from the
# file system
resource.loader=file
file.resource.loader.class=org.apache.velocity.runtime.resource.loader.FileResourceLoader


# additional config for file system loader only.. tell Velocity where the root
# directory is for template loading.  You can define multiple root directories
# if you wish, I just use the one here.  See the text below for a note about
# the ${webapp.root}
file.resource.loader.path=${webapp.root}/WEB-INF/velocity


# caching should be 'true' in production systems, 'false' is a development
# setting only.  Change to 'class.resource.loader.cache=false' for classpath
# loading
file.resource.loader.cache=false

# override default logging to direct velocity messages
# to our application log for example.  Assumes you have 
# defined a log4j.properties file
runtime.log.logsystem.log4j.category=com.mycompany.myapplication 

13.4.3.3. Web应用的根目录标记

上面在配置文件资源载入时,使用一个标记${webapp.root}来代表Web应用在文件系统中的根目录。这个标记在作为属性提供给Velocity之前,会被Spring的代码解释成和操作系统有关的实际路径。这种文件资源的载入方式在一些服务器中是不可移植的。如果你认为可移植性很重要,可以给VelocityConfigurer定义不同的“appRootMarker”,来修改根目录标记本身的名字。Spring的文档对此有详细表述。

13.4.3.4. 另一种可选的属性规范

作为选择,你可以用下面的内嵌属性来代替Velocity配置bean中的“configLocation”属性,从而直接指定Velocity属性。

<property name="velocityProperties">
    <props>
        <prop key="resource.loader">file</prop>
        <prop key="file.resource.loader.class">org.apache.velocity.runtime.resource.loader.FileResourceLoader</prop>
        <prop key="file.resource.loader.path">${webapp.root}/WEB-INF/velocity</prop>
        <prop key="file.resource.loader.cache">false</prop>
    </props>
</property>

13.4.3.5. 缺省配置(文件资源载入)

注意从Spring 1.0-m4起,你可以不使用属性文件或内嵌属性来定义模版文件的载入,你可以把下面的属性放在Velocity配置bean中。

<property name="resourceLoaderPath"><value>/WEB-INF/velocity/</value></property>

13.4.4. 视图配置

配置的最后一步是定义一些视图,这些视图和Velocity模版一起被显示。视图被定义在Spring上下文文件中。正如前面提到的,这个例子使用XML文件定义视图bean,但是也可以使用属性文件(ResourceBundle)来定义。视图定义文件的名字被定义在WEB-INF/frontcontroller-servlet.xml文件的ViewResolver的bean中。

<!--
  Views can be hierarchical, here's an example of a parent view that 
  simply defines the class to use and sets a default template which
  will normally be overridden in child definitions.
-->
<bean id="parentVelocityView" class="org.springframework.web.servlet.view.velocity.VelocityView">
    <property name="url"><value>mainTemplate.vm</value></property>        
</bean>

<!--
  - The main view for the home page.  Since we don't set a template name, the value
  from the parent is used.
-->
<bean id="welcomeView" parent="parentVelocityView">
  <property name="attributes">
      <props>
          <prop key="title">My Velocity Home Page</prop>
      </props>
  </property>     
</bean>  

<!--
  - Another view - this one defines a different velocity template.
-->
<bean id="secondaryView" parent="parentVelocityView">
    <property name="url"><value>secondaryTemplate.vm</value></property>  
    <property name="attributes">
        <props>
            <prop key="title">My Velocity Secondary Page</prop>
        </props>
    </property>    
</bean>

13.4.5. 创建Velocity模版

最后,你需要做的就是创建Velocity模版。我们定义的视图引用了两个模版,mainTemplate.vm和secondaryTemplate.vm。属性文件velocity.proeprties定义这两个文件被放在WEB-INF/velocity/下。如果你在velocity.properties中选择通过classpath载入,它们应该被放在缺省包的目录下,(WEB-INF/classes),或者WEB-INF/lib下的jar文件中。下面就是我们的‘secondaryView’看上去的样子(简化了的HTML文件)。

## $title is set in the view definition file for this view.
<html>
    <head><title>$title</title></head>
    <body>
        <h1>This is $title!!</h1>

        ## model objects are set in the controller and referenced
        ## via bean properties o method names.  See the Velocity 
        ## docs for info

        Model Value: $model.value
        Model Method Return Value: $model.getReturnVal()

    </body>
</html>

现在,当你的控制器返回一个ModelAndView包含“secondaryView”时,Velocity就会工作,将上面的页面转化为普通的HTML页面。

13.4.6. 表单处理

Spring提供一个标签库给JSP页面使用,其中包含了<spring:bind>标签。这个标签主要使表单能够显示在web层或业务层中的Validator验证时产生的出错消息。这种行为可以被Velocity宏和其他的Spring功能模拟实现。

13.4.6.1. 验证错误

通过表单验证而产生的出错消息可以从属性文件中读取,这有助于维护和国际化它们。Spring以它自己的方式处理这些,关于它的工作方式,你可以参考MVC指南或javadoc中的相关内容。为了访问这些出错消息,需要把RequestContext对象暴露给VelocityContext中的Velocity模版。修改你在views.properties或views.xml中的模版定义,给一个名字到它的attributes里(有了名字就可以被访问到)。

<bean id="welcomeView" parent="parentVelocityView">
    <property name="requestContextAttribute"><value>rc</value></property>  
    <property name="attributes">
        <props>
            <prop key="title">My Velocity Home Page</prop>
        </props>
    </property>     
</bean>

在我们前面例子的基础上,上面的例子将RequestContext属性命名为rc。这样从这个视图继承的所有Velocity视图都可以访问$rc

13.4.6.2. Velocity的宏

接下来,需要定义一个Velocity宏。既然宏可以在几个Velocity模版(HTML表单)中重用,那么完全可以把宏定义在一个宏文件中。创建宏的详细信息,参考Velocity文档。

下面的代码应该放在你的Velocity模版根目录的VM_global_library.vm文件中。

#*
 * showerror
 *
 * display an error for the field name supplied if one exists
 * in the supplied errors object.
 *
 * param $errors the object obtained from RequestContext.getErrors( "formBeanName" )
 * param $field the field name you want to display errors for (if any)
 *
 *#
#macro( showerror $errors $field )
    #if( $errors )
        #if( $errors.getFieldErrors( $field ))
            #foreach($err in $errors.getFieldErrors( $field ))
                <span class="fieldErrorText">$rc.getMessage($err)</span><br />
            #end
        #end
    #end
#end      

13.4.6.3. 将出错消息和HTML的域关联起来

最后,在你的HTML表单中,你可以使用和类似下面的代码为每个输入域显示所绑定的出错消息。

## set the following variable once somewhere at the top of
## the velocity template
#set ($errors=$rc.getErrors("commandBean"))
<html>
...
<form ...>
    <input name="query" value="$!commandBean.query"><br>
    #showerror($errors "query")
</form>
...
</html>        

13.4.7. 总结

总之,下面是上面那个例子的文件目录结构。只有一部分被显示,其他一些必要的目录没有显示出来。文件定位出错很可能是Velocity视图不能工作的主要原因,其次在视图中定义了错误的属性也是很常见的原因。

ProjectRoot
  |
  +- WebContent
      |
      +- WEB-INF
          |
          +- lib
          |   |
          |   +- velocity-1.3.1.jar
          |   +- spring.jar
          |
          +- velocity
          |   |
          |   +- VM_global_library.vm
          |   +- mainTemplate.vm
          |   +- secondaryTemplate.vm
          |
          +- frontcontroller-servlet.xml
          +- frontcontroller-views.xml
          +- velocity.properties

13.5. XSLT视图

XSLT一种用于XML文件的转换语言,作为web应用的一种表现层技术非常流行。如果你的应用本身需要处理XML文件,或者你的数据模型很容易转换成XML文件,XSLT就是一个不错的选择。下面介绍如何生成XML文档用作模型数据,以及如何在Spring应用中使用XSLT转换它们。

13.5.1. My First Words

这个Spring应用的例子在控制器中创建一组单词,并把它们加到数据模型的映射表中。这个映射表和我们XSLT视图的名字一起被返回。关于Spring中Controller的详细信息,参考第 12.3 节 “控制器” 。 XSLT视图会把这组单词生成一个简单XML文档用于转换。

13.5.1.1. Bean的定义

对于一个简单的Spring应用,配置是标准的。DispatcherServlet配置文件包含一个ViewResolver,URL映射和一个控制器bean..

<bean id="homeController"class="xslt.HomeController"/> 

..它实现了我们单词的产生“逻辑”。

13.5.1.2. 标准MVC控制器代码

控制器逻辑被封装在AbstractController的子类中,包含象下面这样的处理器方法。

protected ModelAndView handleRequestInternal(
    HttpServletRequest req,
    HttpServletResponse resp)
    throws Exception {
        
    Map map = new HashMap();
    List wordList = new ArrayList();
        
    wordList.add("hello");
    wordList.add("world");
       
    map.put("wordList", wordList);
      
    return new ModelAndView("home", map);
} 

 

到目前为止,我们没有做任何XSLT特定的东西。模型数据的创建方式和其他Spring的 MVC应用相同。现在根据不同的应用配置,这组单词被作为请求属性交给JSP/JSTL处理,或者作为VelocityContext里的对象,交给Velocity处理。为了使XSLT能处理它们,它们必须得转换成某种XML文档。有一些软件包可以自动DOM化一个对象图,但在Spring中,你可以用任何方式把你的模型转换成DOM树。这样可以避免使XML转换决定你模型数据结构,这在使用工具管理DOM化过程的时候是很危险的。

13.5.1.3. 把模型数据转换成XML文档

为了从我们的单词列表或其他模型数据中创建DOM文档,我们继承org.springframework.web.servlet.view.xslt.AbstractXsltView。同时,我们必须实现抽象方法createDomNode()。传给它的第一个参数就是我们的数据模型的Map。下面是我们这个应用中HomePage类的源程序清单 - 它使用JDOM来创建XML文档,然后在转换成所需要的W3C节点,这仅仅是因为我发现JDOM(和Dom4J)的API比W3C的API简单。

package xslt;

// imports omitted for brevity

public class HomePage extends AbstractXsltView {

    protected Node createDomNode( 
        Map model, String rootName, HttpServletRequest req, HttpServletResponse res
    ) throws Exception {
        
        org.jdom.Document doc = new org.jdom.Document();
        Element root = new Element(rootName);
        doc.setRootElement(root);

        List words = (List) model.get("wordList");
        for (Iterator it = words.iterator(); it.hasNext();) {
            String nextWord = (String) it.next();
            Element e = new Element("word");
            e.setText(nextWord);
            root.addContent(e);
        }

        // convert JDOM doc to a W3C Node and return
        return new DOMOutputter().output( doc );
    }

}

 

13.5.1.3.1. 添加样式表参数

你的视图子类可以定义一些name/value组成的参数,这些参数将被加到转换对象中。参数的名字必须符合你在XSLT模版中使用<xsl:param name="myParam">defaultValue</xsl:param>格式定义的参数名。为了指定这些参数,可以从AbstractXsltView中重载方法getParameters(),并返回包含name/value组合的Map

13.5.1.3.2. 格式化日期和货币

不象JSTL和Velocity,XSLT对和本地化相关的货币和日期格式化支持较弱。Spring为此提供了一个帮助类,让你在createDomNode()中使用,从而获得这些支持。详细请参考org.springframework.web.servlet.view.xslt.FormatHelper的javadoc。

13.5.1.4. 定义视图属性

下面是单视图应用的属性文件views.properties(如果你使用基于XML的视图解析器,比如上面例子中的Velocity,它等价于XML定义),如我们的“My First Words”..

home.class=xslt.HomePage
home.stylesheetLocation=/WEB-INF/xsl/home.xslt
home.root=words

这儿,你可以看到视图是如何绑定在由属性“.class”定义的HomePage类上的,HomePage类处理数据模型的DOM化操作。属性“stylesheetLocation”指定了将XML文档转换成HTML文档时所需要的XSLT文件,而最后一个属性“.root”指定了XML文档根节点的名字。它被上面的HomePage类作为第二个参数传递给createDomNode方法。

13.5.1.5. 文档转换

最后,我们定义了XSLT的代码来转换上面的XML文档。在views.properties文件中指定了这个XSLT文件home.xslt存放在war文件里的WEB-INF/xsl下。

<?xml version="1.0"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text/html" omit-xml-declaration="yes"/>

    <xsl:template match="/">
        <html>
            <head><title>Hello!</title></head>
            <body>

                <h1>My First Words</h1>
                <xsl:for-each select="wordList/word">
                    <xsl:value-of select="."/><br />
                </xsl:for-each> 

            </body>
        </html>
    </xsl:template>

</xsl:stylesheet>

 

13.5.2. 总结

下面的WAR文件结构简单列了一些上面所提到的文件和它们在WAR文件中的位置。

ProjectRoot
  |
  +- WebContent
      |
      +- WEB-INF
          |
          +- classes
          |    |
          |    +- xslt
          |    |   |
          |    |   +- HomePageController.class 
          |    |   +- HomePage.class
          |    |
          |    +- views.properties
          |
          +- lib
          |   |
          |   +- spring.jar
          |
          +- xsl
          |   |
          |   +- home.xslt
          |
          +- frontcontroller-servlet.xml

当然,你还需要保证XML解析器和XSLT引擎在classpath中可以被找到。JDK 1.4会缺省提供它们,并且大多数J2EE容器也会提供它们,但是这也是一些已知的可能引起错误的原因。

13.6. 文档视图 (PDF/Excel)

13.6.1. 简介

HTML页面并不总是向用户显示数据输出的最好方式,Spring支持从数据动态生成PDF或Excel文件,并使这一过程变得简单。文档本身就是视图,从服务器以流的方式加上内容类型返回文档,客户端PC只要运行电子表格软件或PDF浏览软件就可以浏览。

为了使用Excel电子表格,你需要在你的classpath中加入‘poi’库文件,而对PDF文件,则需要iText.jar文件。它们都包含在Spring的主发布包中。

13.6.2. 配置和安装

基于文档的视图的处理方式和XSLT视图几乎完全相同,下面的部分将以前面的例子为基础,演示了XSLT例子中的控制器是如何使用相同的数据模型生成PDF文档和Excel电子表格(它们可以在Open Office中打开或编辑)。

13.6.2.1. 文档视图定义

首先,让我们来修改一下view.properties文件(或等价的xml定义),给两种文档类型都添加一个视图定义。加上刚才XSLT视图例子的内容,整个文件如下。

home.class=xslt.HomePage
home.stylesheetLocation=/WEB-INF/xsl/home.xslt
home.root=words

xl.class=excel.HomePage

pdf.class=pdf.HomePage

如果你添加你的数据到一个模版电子表格,必须在视图定义的‘url’属性中指定模版位置。

13.6.2.2. 控制器代码

我们用的控制器代码和前面XSLT例子中用的一样,除了视图的名字。当然,你可以干得巧妙一点,使它基于URL参数或者其他逻辑-这证明了Spring的确在分离视图和控制器方面非常出色!

13.6.2.3. 用于Excel视图的视图子类化

正入我们在XSLT例子中做的,为了在生成输出文档的过程中实现定制的行为,我们将继承合适的抽象类。对于Excel,这包括提供一个org.springframework.web.servlet.view.document.AbstractExcelView的子类,并实现buildExcelDocument方法。

下面是一个我们Excel视图的源程序清单,它在电子表格中每一行的第一列中显示模型map中的单词。

package excel;

// imports omitted for brevity

public class HomePage extends AbstractExcelView {

    protected void buildExcelDocument(
        Map model,
        HSSFWorkbook wb,
        HttpServletRequest req,
        HttpServletResponse resp)
        throws Exception {
    
        HSSFSheet sheet;
        HSSFRow sheetRow;
        HSSFCell cell;

        // Go to the first sheet
        // getSheetAt: only if wb is created from an existing document
        //sheet = wb.getSheetAt( 0 );
        sheet = wb.createSheet("Spring");
        sheet.setDefaultColumnWidth((short)12);

        // write a text at A1
        cell = getCell( sheet, 0, 0 );
        setText(cell,"Spring-Excel test");

        List words = (List ) model.get("wordList");
        for (int i=0; i < words.size(); i++) {
            cell = getCell( sheet, 2+i, 0 );
            setText(cell, (String) words.get(i));

        }
    }
}

 

如果你现在修改控制器使它返回xl作为视图的名字(return new ModelAndView("xl", map);),并且运行你的应用,当你再次对该页面发起请求时,Excel电子表格被创建,自动下载。

13.6.2.4. 用于PDF视图的视图子类化

单词列表的PDF版本就更为简单了。这次,需要象下面一样继承org.springframework.web.servlet.view.document.AbstractPdfView,并实现buildPdfDocument()方法。

package pdf;

// imports omitted for brevity

public class PDFPage extends AbstractPdfView {

    protected void buildPdfDocument(
        Map model,
        Document doc,
        PdfWriter writer,
        HttpServletRequest req,
        HttpServletResponse resp)
        throws Exception {
        
        List words = (List) model.get("wordList");
        
        for (int i=0; i<words.size(); i++)
            doc.add( new Paragraph((String) words.get(i)));
    
    }
}

同样修改控制器,使它通过return new ModelAndView("pdf", map);返回一个pdf视图;并在你的应用中重新载入该URL。这次就会出现一个PDF文档,显示存储在模型数据的map中的单词。

13.7. Tapestry

Tapestry是Apache Jakarta项目(http://jakarta.apache.org/tapestry)下的一个面向组件的web应用框架。Spring框架是围绕轻量级容器概念建立的J2EE应用框架。虽然Spring它自己的web表现层功能也很丰富,但是使用Tapestry作为web表现层,Spring容器作为底层构建的J2EE应用有许多独特的优势。这一节将详细介绍使用这两种框架的最佳实现。这里假设你熟悉Tapestry和Spring框架的基础知识,这里就不再加以解释了。对于Tapestry和Spring框架的一般性介绍文档,可以在它们的网站找到。

13.7.1. 架构

一个由Tapestry和Spring构建的典型分层的J2EE应用包括一个上层的Tapestry表现层和许多底部层次构成,它们存在于一个或多个Spring应用上下文中。

  • 用户界面层:

    - 主要关注用户界面的内容

    - 包含某些应用逻辑

    - 由Tapestry提供

    - 除了通过Tapestry提供的用户界面,这一层的代码访问实现业务层接口的对象。实现对象由Spring应用上下文提供。

  • 业务层:

    - 应用相关的“业务”代码

    - 访问域对象,并且使用Mapper API从某种数据存储(数据库)中存取域对象

    - 存在在一个或多个Spring上下文中

    - 这层的代码以一种应用相关的方式操作域模型中的对象。它通过这层中的其它代码和Mapper API工作。这层中的对象由某个特定的mapper实现通过应用上下文提供。

    - 既然这层中的代码存在在Spring上下文中,它由Spring上下文提供事务处理,而不自己管理事务。

  • 域模型:

    - 问题域相关的对象层次,这些对象处理和问题域相关的数据和逻辑

    - 虽然域对象层次被创建时考虑到它会被某种方式持久化,并且为此定义一些通用的约束(例如,双向关联),它通常并不知道其它层次的情况。因此,它可以被独立地测试,并且在产品和测试这两种不同的mapping实现中使用。

    - 这些对象可以是独立的,也可以关联Spring应用上下文以发挥它的优势,例如隔离,反向控制,不同的策略实现,等等。

  • 数据源层:

    - Mapper API(也称为Data Access Objects):是一种将域模型持久化到某种数据存储(一般是数据库,但是也可以是文件系统,内存,等等)的API。

    - Mapper API实现:是指Mapper API的一个或多个特定实现,例如,Hibernate的mapper,JDO的mapper,JDBC的mapper,或者内存mapper。

    - mapper实现存在在一个或多个Spring应用上下文中。一个业务层对象需要应用上下文的mapper对象才能工作。

  • 数据库,文件系统,或其它形式的数据存储:

    - 在域模型中的对象根据一个或多个mapper实现可以存放在不止一个数据存储中

    - 数据存储的方式可以是简单的(例如,文件系统),或者有它域模型自己的数据表达(例如,一个数据库中的schema)。但是它不知道其它层次的情况。

13.7.2. 实现

真正的问题(本节所需要回答的),是Tapestry页面是如何访问业务实现的,业务实现仅仅是定义在Spring应用上下文实例中的bean。

13.7.2.1. 应用上下文示例

假设我们以xml格式定义的下面的应用上下文:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
        "http://www.springframework.org/dtd/spring-beans.dtd">
 
<beans>
 
    <!-- ========================= GENERAL DEFINITIONS ========================= -->
 
    <!-- ========================= PERSISTENCE DEFINITIONS ========================= -->
 
    <!-- the DataSource -->
    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName"><value>java:DefaultDS</value></property>
        <property name="resourceRef"><value>false</value></property>
    </bean>
 
    <!-- define a Hibernate Session factory via a Spring LocalSessionFactoryBean -->
    <bean id="hibSessionFactory" 
        class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
        <property name="dataSource"><ref bean="dataSource"/></property>
    </bean>
 
    <!--
     - Defines a transaction manager for usage in business or data access objects.
     - No special treatment by the context, just a bean instance available as reference
     - for business objects that want to handle transactions, e.g. via TransactionTemplate.
     -->
    <bean id="transactionManager" 
        class="org.springframework.transaction.jta.JtaTransactionManager">
    </bean>
 
    <bean id="mapper" 
        class="com.whatever.dataaccess.mapper.hibernate.MapperImpl">
        <property name="sessionFactory"><ref bean="hibSessionFactory"/></property>
    </bean>
   
    <!-- ========================= BUSINESS DEFINITIONS ========================= -->
 
    <!-- AuthenticationService, including tx interceptor -->
    <bean id="authenticationServiceTarget"
        class="com.whatever.services.service.user.AuthenticationServiceImpl">
        <property name="mapper"><ref bean="mapper"/></property>
    </bean>
    <bean id="authenticationService" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager"><ref bean="transactionManager"/></property>
        <property name="target"><ref bean="authenticationServiceTarget"/></property>
        <property name="proxyInterfacesOnly"><value>true</value></property>
        <property name="transactionAttributes">
            <props>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>  
 
    <!-- UserService, including tx interceptor -->
    <bean id="userServiceTarget"
        class="com.whatever.services.service.user.UserServiceImpl">
        <property name="mapper"><ref bean="mapper"/></property>
    </bean>
    <bean id="userService" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager"><ref bean="transactionManager"/></property>
        <property name="target"><ref bean="userServiceTarget"/></property>
        <property name="proxyInterfacesOnly"><value>true</value></property>
        <property name="transactionAttributes">
            <props>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>  
 
 </beans>

在Tapestry应用中,我们需要载入这个应用上下文,并允许Tapestry页面访问authenticationService和userService这两个bean,它们分别实现了AuthenticationService接口和UserService接口。

13.7.2.2. 在Tapestry页面中获取bean

在这点上,web应用可以调用Spring的静态工具方法WebApplicationContextUtils.getApplicationContext(servletContext)来获取应用上下文,参数servletContext是J2EE Servlet规范定义的标准ServletContext。因此,页面获取例如UserService实例的一个简单方法就象下面的代码:

    WebApplicationContext appContext = WebApplicationContextUtils.getApplicationContext(
        getRequestCycle().getRequestContext().getServlet().getServletContext());
    UserService userService = appContext.getBean("userService");
    ... some code which uses UserService

这个方法可以工作。将大部分逻辑封装在页面或组件基类的一个方法中可以减少很多冗余。然而,这在某些方面违背了Spring所倡导的反向控制方法,而应用中其它层次恰恰在使用反向控制,因为你希望页面不必向上下文要求某个名字的bean,事实上,页面也的确对上下文一无所知。

幸运的是,有一个方法可以做到这一点。这是因为Tapestry已经提供一种方法给页面添加声明属性,事实上,以声明方式管理一个页面上的所有属性是首选的方法,这样Tapestry能够将属性的生命周期作为页面和组件生命周期的一部分加以管理。

13.7.2.3. 向Tapestry暴露应用上下文

首先我们需要Tapestry页面组件在没有ServletContext的情况下访问ApplicationContext;这是因为在页面/组件生命周期里,当我们需要访问ApplicationContext时,ServletContext并不能被页面很方便地访问到,所以我们不能直接使用WebApplicationContextUtils.getApplicationContext(servletContext)。一个方法就是实现一个特定的Tapestry的IEngine来暴露它:

package com.whatever.web.xportal;
...
import ...
...
public class MyEngine extends org.apache.tapestry.engine.BaseEngine {
 
    public static final String APPLICATION_CONTEXT_KEY = "appContext";
 
    /**
     * @see org.apache.tapestry.engine.AbstractEngine#setupForRequest(org.apache.tapestry.request.RequestContext)
     */
    protected void setupForRequest(RequestContext context) {
        super.setupForRequest(context);
     
        // insert ApplicationContext in global, if not there
        Map global = (Map) getGlobal();
        ApplicationContext ac = (ApplicationContext) global.get(APPLICATION_CONTEXT_KEY);
        if (ac == null) {
            ac = WebApplicationContextUtils.getWebApplicationContext(
                context.getServlet().getServletContext()
            );
            global.put(APPLICATION_CONTEXT_KEY, ac);
        }
    }
}

这个engine类将Spring应用上下文作为“appContext”属性存放在Tapestry应用的“Global”对象中。在Tapestry应用定义文件中必须保证这个特殊的IEngine实例在这个Tapestry应用中被使用。例如,

file: xportal.application:
 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application PUBLIC 
    "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<application
    name="Whatever xPortal"
    engine-class="com.whatever.web.xportal.MyEngine">
</application>

 

13.7.2.4. 组件定义文件

现在在我们的页面或组件定义文件(*.page或*.jwc)中,我们仅仅添加property-specification元素从ApplicatonContext中获取bean,并为这些bean创建页面或组件属性。例如:

    <property-specification name="userService"
                            type="com.whatever.services.service.user.UserService">
        global.appContext.getBean("userService")
    </property-specification>
    <property-specification name="authenticationService"
                            type="com.whatever.services.service.user.AuthenticationService">
        global.appContext.getBean("authenticationService")
    </property-specification>

在property-specification中定义的OGNL表达式使用上下文中的bean来指定属性的初始值。整个页面定义文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC 
    "-//Apache Software Foundation//Tapestry Specification 3.0//EN" 
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
     
<page-specification class="com.whatever.web.xportal.pages.Login">
 
    <property-specification name="username" type="java.lang.String"/>
    <property-specification name="password" type="java.lang.String"/>
    <property-specification name="error" type="java.lang.String"/>
    <property-specification name="callback" type="org.apache.tapestry.callback.ICallback" persistent="yes"/>
    <property-specification name="userService"
                            type="com.whatever.services.service.user.UserService">
        global.appContext.getBean("userService")
    </property-specification>
    <property-specification name="authenticationService"
                            type="com.whatever.services.service.user.AuthenticationService">
        global.appContext.getBean("authenticationService")
    </property-specification>
   
    <bean name="delegate" class="com.whatever.web.xportal.PortalValidationDelegate"/>
 
    <bean name="validator" class="org.apache.tapestry.valid.StringValidator" lifecycle="page">
        <set-property name="required" expression="true"/>
        <set-property name="clientScriptingEnabled" expression="true"/>
    </bean>
 
    <component id="inputUsername" type="ValidField">
        <static-binding name="displayName" value="Username"/>
        <binding name="value" expression="username"/>
        <binding name="validator" expression="beans.validator"/>
    </component>
   
    <component id="inputPassword" type="ValidField">
        <binding name="value" expression="password"/>
       <binding name="validator" expression="beans.validator"/>
       <static-binding name="displayName" value="Password"/>
       <binding name="hidden" expression="true"/>
    </component>
 
</page-specification>

 

13.7.2.5. 添加抽象访问方法

现在在页面或组件本身的Java类定义中,我们所需要做的是为我们定义的属性添加抽象getter方法。当Tapestry真正载入页面或组件时,Tepestry会对类文件作一些运行时的代码处理,添加已定义的属性,挂接抽象getter方法到新创建的域上。例如:

    // our UserService implementation; will come from page definition
    public abstract UserService getUserService();
    // our AuthenticationService implementation; will come from page definition
    public abstract AuthenticationService getAuthenticationService();

这个例子的login页面的完整Java类如下:

package com.whatever.web.xportal.pages;
 
/**
 *  Allows the user to login, by providing username and password.
 *  After succesfully logging in, a cookie is placed on the client browser
 *  that provides the default username for future logins (the cookie
 *  persists for a week).
 */
public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {
 
    /** the key under which the authenticated user object is stored in the visit as */
    public static final String USER_KEY = "user";
   
    /**
     * The name of a cookie to store on the user's machine that will identify
     * them next time they log in.
     **/
    private static final String COOKIE_NAME = Login.class.getName() + ".username";  
    private final static int ONE_WEEK = 7 * 24 * 60 * 60;
 
    // --- attributes
 
    public abstract String getUsername();
    public abstract void setUsername(String username);
 
    public abstract String getPassword();
    public abstract void setPassword(String password);
 
    public abstract ICallback getCallback();
    public abstract void setCallback(ICallback value);
    
    public abstract UserService getUserService();
 
    public abstract AuthenticationService getAuthenticationService();
 
    // --- methods
 
    protected IValidationDelegate getValidationDelegate() {
        return (IValidationDelegate) getBeans().getBean("delegate");
    }
 
    protected void setErrorField(String componentId, String message) {
        IFormComponent field = (IFormComponent) getComponent(componentId);
        IValidationDelegate delegate = getValidationDelegate();
        delegate.setFormComponent(field);
        delegate.record(new ValidatorException(message));
    }
 
    /**
     *  Attempts to login. 
     *
     *  <p>If the user name is not known, or the password is invalid, then an error
     *  message is displayed.
     *
     **/
    public void attemptLogin(IRequestCycle cycle) {
     
        String password = getPassword();
 
        // Do a little extra work to clear out the password.
 
        setPassword(null);
        IValidationDelegate delegate = getValidationDelegate();
 
        delegate.setFormComponent((IFormComponent) getComponent("inputPassword"));
        delegate.recordFieldInputValue(null);
 
        // An error, from a validation field, may already have occured.
 
        if (delegate.getHasErrors())
            return;
 
        try {
            User user = getAuthenticationService().login(getUsername(), getPassword());
           loginUser(user, cycle);
        }
        catch (FailedLoginException ex) {
            this.setError("Login failed: " + ex.getMessage());
            return;
        }
    }
 
    /**
     *  Sets up the {@link User} as the logged in user, creates
     *  a cookie for their username (for subsequent logins),
     *  and redirects to the appropriate page, or
     *  a specified page).
     *
     **/
    public void loginUser(User user, IRequestCycle cycle) {
     
        String username = user.getUsername();
 
        // Get the visit object; this will likely force the
        // creation of the visit object and an HttpSession.
 
        Map visit = (Map) getVisit();
        visit.put(USER_KEY, user);
 
        // After logging in, go to the MyLibrary page, unless otherwise
        // specified.
 
        ICallback callback = getCallback();
 
        if (callback == null)
            cycle.activate("Home");
        else
            callback.performCallback(cycle);
 
        // I've found that failing to set a maximum age and a path means that
        // the browser (IE 5.0 anyway) quietly drops the cookie.
 
        IEngine engine = getEngine();
        Cookie cookie = new Cookie(COOKIE_NAME, username);
        cookie.setPath(engine.getServletPath());
        cookie.setMaxAge(ONE_WEEK);
 
        // Record the user's username in a cookie
 
        cycle.getRequestContext().addCookie(cookie);
 
        engine.forgetPage(getPageName());
    }
   
    public void pageBeginRender(PageEvent event) {
        if (getUsername() == null)
            setUsername(getRequestCycle().getRequestContext().getCookieValue(COOKIE_NAME));
    }
}

 

13.7.3. 小结

在这个例子中,我们用声明的方式将定义在Spring的ApplicationContext中业务bean能够被页面访问。页面类并不知道业务实现从哪里来,事实上,也很容易转移到另一个实现,例如为了测试。这样的反向控制是Spring框架的主要目标和优点,在这个Tapestry应用中,我们在J2EE栈上自始至终使用反向控制。

 

 

 

 

 

 

第 14 章 JMS支持

14.1. 介绍

Spring提供一个用于简化JMS API使用的抽象层框架,并且对用户屏蔽JMS API中从1.0.2到1.1版本之间的不同。

JMS大体上被分为两个功能块,消息生产和消息消费。在J2EE环境,由消息驱动的bean提供了异步消费消息的能力 。而在独立的应用中,则必须创建MessageListener或ConnectionConsumer来消费消息。 JmsTemplate的主要功能就是产生消息。Spring的未来版本将会提供,在一个独立的环境中处理异步消息。

org.springframework.jms.core包提供使用JMS的核心功能。 就象为JDBC提供的JdbcTemplate一样, 它提供了JMS模板类来处理资源的创建和释放以简化JMS的使用。 这个Spring的模板类的公共设计原则就是通过提供helper方法去执行公共的操作, 以及将实际的处理任务委派到用户实现的回调接口上,从而以完成更复杂的操作。 JMS模板遵循这样的设计原则。这些类提供众多便利的方法来发送消息、异步地接收消息、 将JMS会话和消息产生者暴露给用户。

org.springframework.jms.support包提供JMSException的转换功能。 它将checked JMSException级别转换到一个对应的unchecked异常级别, 任何checked的javax.jms.JMSException异常的子类都被包装到unchecked的UncategorizedJmsException。 org.springframework.jms.support.converter 包提供一个MessageConverter的抽象进行Java对象和JMS消息之间的转换。 org.springframework.jms.support.destination提供多种管理JMS目的地的策略, 例如为存储在JNDI中的目的地提供一个服务定位器。

最后,org.springframework.jms.connection包提供一个适合在独立应用中使用的 ConnectionFactory的实现。它还为JMS提供了一个Spring的PlatformTransactionManager的实现。 这让JMS作为一个事务资源和Spring的事务管理机制可以集成在一起使用。

14.2. 域的统一

JMS主要发布了两个规范版本,1.0.2和1.1。JMS1.0.2定义了两种消息域, 点对点(队列)和发布/订阅(主题)。 JMS1.0.2的API为每个消息领域提供了类似的类体系来处理这两种不同的消息域。 结果,客户端应用在使用JMS API时要了解是在使用哪种消息域。 JMS 1.1引进了统一域的概念来最小化这两种域之间功能和客户端API的差别。 举个例子,如果你使用的是一个JMS 1.1的消息供应者, 你可以使用同一个Session事务性地在一个域接收一个消息后并且从另一个域中产生一个消息。

JMS 1.1的规范发布于2002年4月,并且在2003年11月成为J2EE 1.4的一个组成部分, 结果,现在大多数使用的应用服务器只支持JMS 1.0.2的规范.

14.3. JmsTemplate

这里为JmsTemplate提供了两个实现。JmsTemplate类使用JMS 1.1的API, 而子类JmsTemplate102使用了JMS 1.0.2的API。

使用JmsTemplate的代码只需要实现规范中定义的回调接口。 在JmsTemplate中通过调用代码让MessageCreator回调接口用所提供的会话(Session)创建消息。 然而,为了顾及更复杂的JMS API应用,回调接口SessionCallback将JMS会话提供给用户, 并且暴露Session和MessageProducer。

JMS API暴露两种发送方法,一种接受交付模式、优先级和存活时间作为服务质量(QOS)参数, 而另一种使用缺省值作为QOS参数(无需参数)方式。由于在JmsTemplate中有多种发送方法, QOS参数用bean属性进行暴露设置,从而避免在一系列发送方法中重复。同样地, 使用setReceiveTimeout属性值来设置用于异步接收调用的超时值。

某些JMS供应者允许通过ConnectionFactory的配置来设置缺省的QOS值。 这样在调用MessageProducer的发送方法send(Destination destination, Message message) 时效率更高,因为调用时直接会使用QOS缺省值,而不再用JMS规范中定义的值。 所以,为了提供对QOS值域的统一管理,JmsTemplate必须通过设置布尔值属性isExplicitQosEnabled 为true,使它能够使用自己的QOS值。

14.3.1. ConnectionFactory

JmsTemplate请求一个对ConnectionFactory的引用。 ConnectionFactory是JMS规范的一部分,并被作为使用JMS的入口。 客户端应用通常作为一个工厂配合JMS提供者去创建连接,并封装一系列的配置参数, 其中一些是和供应商相关的,例如SSL的配置选项。

当在EJB内使用JMS时,供应商提供JMS接口的实现,以至于可以参与声明式事务的管理, 提供连接池和会话池。为了使用这个实现,J2EE容器一般要求你在EJB或servlet部署描述符中将JMS连接工厂声明为 resource-ref。为确保可以在EJB内使用JmsTemplate的这些特性, 客户应用应当确保它能引用其中的ConnectionFactory实现。

Spring提供ConnectionFactory接口的一个实现,SingleConnectionFactory, 它将在所有的createConnection调用中返回一个相同的连接, 并忽略close的调用。这在测试和独立的环境中相当有用, 因为同一个连接可以被用于多个JmsTemplate调用以跨越多个事务。 SingleConnectionFactory接受一个通常来自JNDI的标准ConnectionFactory的引用。

14.3.2. 事务管理

Spring为单个JMS ConnectionFactory提供一个JmsTransactionManager来管理事务。 它允许JMS应用可以利用第7章中描述的Spring的事务管理特性。JmsTransactionManager 从指定的ConnectionFactory将一个Connection/Session对绑定到线程。然而,在一个J2EE环境, ConnectionFactory将缓存连接和会话,所以被绑定到线程的实例依赖于缓存的行为。 在一个独立的环境,使用Spring的SingleConnectionFactory将导致使用单独的JMS连接, 而且每个连接都有自己的会话。JmsTemplate也能和JtaTransactionManager 以及XA-capable的JMS ConnectionFactory一起使用以完成分布式事务。

当使用JMS API从连接创建一个Session时,跨越管理性和非管理性事务的复用代码可能会让人困惑。这是因为JMS API只提供一个工厂方法来创建会话,并且它要求事务和确认模式的值。在受管理的环境下,由事务结构环境负责设置这些值,这样在供应商包装的JMS连接中可以忽略这些值。当在一个非管理性的环境中使用JmsTemplate时,你可以通过使用属性SessionTransactedSessionAcknowledgeMode来指定这些值。当在JmsTemplate中使用PlatformTransactionManager时,模板将一直被赋予一个事务性JMS会话。

14.3.3. Destination管理

Destination,象ConnectionFactories一样,是可以在JNDI中进行存储和提取的JMS管理对象。当配置一个Spring应用上下文,可以使用JNDI工厂类JndiObjectFactoryBean在你的对象引用上执行依赖注入到JMS Destination。然而,如果在应用中有大量的Destination,或者JMS供应商提供了特有的高级Destination管理特性,这个策略常常显得很笨重。高级Destination管理的例子如创建动态destination或支持destination的命名层次。JmsTemplate将destination名字到JMS destination对象的解析委派到一个DestinationResolver接口的实现。DynamicDestinationResolverJmsTemplate 使用的默认实现,并且提供动态destination解析。同时JndiDestinationResolver作为JNDI包含的destination的服务定位器,并且可选择地退回来使用DynamicDestinationResolver提供的行为。

相当常见的是在一个JMS应用中所使用的destination只有在运行时才知道,因此,当一个应用被部署时,它不能被创建。这经常是因为交互系统组件之间的共享应用逻辑是在运行时按照已知的命名规范创建destination。虽然动态destination的创建不是JMS规范的一部分,但是许多供应商已经提供了这个功能。用户为所建的动态destination定义名字,这样区别于来临时destination,并且动态destination不会被注册到JNDI中。创建动态destination所使用的API在不同的供应商之间差别很大,因为destination所关联的属性是供应商特有的。然而,有时由供应商作出的一个简单的实现选择是忽略JMS规范中的警告,并使用TopicSession的方法createTopic(String topicName)或者QueueSession的方法createQueue(String queueName)来创建一个拥有默认属性的新destination。依赖于供应商的实现,DynamicDestinationResolver可能也能创建一个物理上的destination,而不是只是解析。

布尔属性PubSubDomain被用来配置JmsTemplate使用什么样的JMS域。这个属性的默认值是false,使用点到点的域,也就是队列。在1.0.2的实现中,这个属性值用来决定JmsTemplate将消息发送到一个队列还是主题。这个标志在1.1的实现中对发送操作没有影响。然而,在这两个实现中,这个属性决定了通过DestinationResolver的实现来解析动态destination的行为。

你还可以通过属性DefaultDestination配置一个带有默认destination的JmsTemplate。默认的destination被使用时,它的发送和接收操作不需要指定一个特定的destination。

14.4. 使用JmsTemplate

要开始使用JmsTemplate前,你需要选择JMS 1.0.2的实现,JmsTemplate102,还是JMS 1.1的实现,JmsTemplate。检查一下你的JMS供应者支持那个版本。

14.4.1. 发送消息

JmsTemplate包含许多有用的方法来发送消息。这些发送方法可以使用javax.jms.Destination对象指定destination,也可以使用字符串在JNDI中查找destination。没有destination参数的发送方法使用默认的destination。这里有个例子使用1.0.2的实现发送消息到一个队列。

import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.JmsTemplate102;
import org.springframework.jms.core.MessageCreator;

public class JmsQueueSender {

  private JmsTemplate jt;

  private ConnectionFactory connFactory;

  private Queue queue;

  public void simpleSend() {
    jt = new JmsTemplate102(connFactory, false);
    jt.send(queue, new MessageCreator() {
      public Message createMessage(Session session) throws JMSException {
        return session.createTextMessage("hello queue world");
      }
    });
  }

  public void setConnectionFactory(ConnectionFactory cf) {
      connFactory = cf;
  }

  public void setQueue(Queue q) {
      queue = q;
  }

}
      

这个例子使用MessageCreator回调接口从所提供的会话对象中创建一个文本消息,并且通过一个ConnectionFactory的引用和指定消息域的布尔值来创建JmsTemplate。BeanFactory使用一个没有参数的构造方法和setConnectionFactory/Queue方法来用构造实例。simpleSend方法在下面修改为发送消息到一个主题而不是队列。

public void simpleSend() {
  jt = new JmsTemplate102(connFactory, true);
  jt.send(topic, new MessageCreator() {
     public Message createMessage(Session session) throws JMSException {
        return session.createTextMessage("hello topic world");
     }
  });
}     
      

当在应用上下文中配置JMS 1.0.2时,重要的是记得设定布尔属性 PubSubDomain的值以确定你是要发送到队列还是主题。

方法send(String destinationName, MessageCreator c)让你利用destination的名字发送消息。如果这个名字在JNDI中注册,你应当将模板中的DesinationResolver属性设置为JndiDestinationResolver的一个实例。

如果你创建JmsTemplate并指定一个默认的destination,send(MessageCreator c)发送消息到这个destination。

14.4.2. 同步接收

虽然JMS一般都是应用在异步操作,但它也可能同步接收消息。重载的receive方法就提供这个功能。在同步接收时,调用线程被阻塞直到收到一个消息。这是一个危险的操作,因为调用线程可能会被无限期的阻塞。receiveTimeout属性指定接收者在放弃等待一个消息前要等多久。

14.4.3. 使用消息转换器

为了更容易的发送域模式对象,JmsTemplate有多种将一个Java对象作为消息数据内容的发送方法。在JmsTemplate中重载方法convertAndSendreceiveAndConvert,可以将转换过程委派到MessageConverter接口的一个实例。这个接口定义了一个简单的Java对象和JMS消息之间进行转换的约定。它的默认实现SimpleMessageConverter支持在StringTextMessagebyte[]BytesMesssagejava.util.MapMapMessage之间进行转换。通过使用转换器,你的应用代码可以专注于通过JMS发送或接收的业务对象,并不用为了怎样将它描述为一个JMS消息而费心。

沙箱目前包含MapMessageConverter,它使用反射在JavaBean和MapMessage之间进行转换。你还可以选择使用XML组包的转换器,如JAXB、Castor、XMLBeans或Xstream,来创建一个TextMessage来描述该对象。

消息属性、消息头和消息体的设置,一般不能被封装在一个转换器类中,为了调整它们,接口MessagePostProcessor可以使你在消息转换后,发送前,访问消息。下面的例子展示了如何在一个java.util.Map被转换为消息之后修改一个消息的头和属性。

public void sendWithConversion() {
    Map m = new HashMap();
    m.put("Name", "Mark");
    m.put("Age", new Integer(35));
    jt.convertAndSend("testQueue", m, new MessagePostProcessor() {

         public Message postProcessMessage(Message message)
            throws JMSException {
            message.setIntProperty("AccountID", 1234);
            message.setJMSCorrelationID("123-00001");

            return message;
        }
    });
}
      

这是一个由上面得到的消息

MapMessage={ 
  Header={ 
    ... standard headers ...
    CorrelationID={123-00001} 
  } 
  Properties={ 
    AccountID={Integer:1234}
  } 
  Fields={ 
    Name={String:Mark} 
    Age={Integer:35} 
  } 
}
      

14.4.4. SessionCallback和ProducerCallback

虽然发送操作涵盖了很多普通的使用场景,但是有些情况你需要在JMS Session或MessageProducer上执行多个操作。SessionCallbackProduerCallback分别暴露了JMS Session和Session/MessageProducer对。JmsTemplate的execute()方法会执行这些接口上的回调方法。

 

 

 

 

 

 

 

 

第 15 章 EJB的存取和实现

作为轻量级的容器,Spring常常被认为是EJB的替代品。我们也相信,对于很多 (不一定是绝大多数)应用和用例,相对于通过EJB容器来实现相同的功能而言, Sping作为容器,加上它在事务,ORM和JDBC存取这些领域中丰富的功能支持, Spring的确是更好的选择。

不过,需要特别注意的是,使用了Spring并不是说我们就不能用EJB了, 实际上,Spring大大简化了从中访问和实现EJB组件或只实现(EJB组件)其功能的复杂性。 另外,如果通过Spring来访问EJB组件服务,以后就可以在本地EJB组件,远程EJB组件, 或者是POJO(简单Java对象)这些变体之间透明地切换服务的实现,而不需要修改 客户端的代码。

本章,我们来看看Spring是如何帮助我们访问和实现EJB组件的。尤其是在访问 无状态Session Bean(SLSBs)的时候,Spring特别有用,现在我们就由此开始讨论。

15.1. 访问EJB

15.1.1. 概念

要调用本地或远程无状态Session Bean上的方法,通常客户端的代码必须 进行JNDI查找,得到(本地或远程的)EJB Home对象,然后调用该对象的"create" 方法,才能得到实际的(本地或远程的)EJB对象。前后调用了不止一个EJB组件 上的方法。

为了避免重复的底层调用,很多EJB应用使用了服务定位器(Service Locator) 和业务委托(Bussiness Delegate)模式,这样要比在客户端代码中到处进行JNDI 查找更好些,不过它们的常见的实现都有明显的缺陷。例如:

  • 通常,若是依赖于服务定位器或业务代理单件来使用EJB,则很难对代码进 行测试。

  • 在仅使用了服务定位器模式而不使用业务委托模式的情况下,应用程序 代码仍然需要调用EJB Home组件的create方法,还是要处理由此引入的异常。 导致代码仍然保留了与EJB API的耦合性以及EJB编程模型的复杂性。

  • 实现业务委托模式通常会导致大量的冗余代码,因为我们不得不编写 很多方法,而它们所做的仅仅是调用EJB组件的同名方法。

Spring采用的方法是允许创建并使用代理对象,一般是在Spring的 ApplicationContext或BeanFactory里面进行配置,这样就和业务代理类似,只需要 少量的代码。我们不再需要另外编写额外的服务定位器或JNDI查找的代码,或者是手写 的业务委托对象里面冗余的方法,除非它们可以带来实质性的好处。

15.1.2. 访问本地的无状态Session Bean(SLSB)

假设有一个web控制器需要使用本地EJB组件。我们遵循前人的实践经验, 于是使用了EJB的业务方法接口(Business Methods Interface)模式,这样, 这个EJB组件的本地接口就扩展了非EJB特定的业务方法接口。让我们假定这个 业务方法接口叫MyComponent。

public interface MyComponent {
    ...
}

(使用业务方法接口模式的一个主要原因就是为了保证本地接口和bean的实现类 之间方法签名的同步是自动的。另外一个原因是它使得稍后我们改用基于POJO(简单Java对象) 的服务实现更加容易,只要这样的改变是有利的。当然,我们也需要实现 本地Home接口,并提供一个Bean实现类,使其实现接口SessionBean和业务方法接口 MyComponent。现在为了把我们Web层的控制器和EJB的实现链接起来,我们唯一要写 的Java代码就是在控制器上公布一个形参为MyComponent的setter方法。这样就可以 把这个引用保存在控制器的一个实例变量中。

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
    this.myComponent = myComponent;
}

然后我们可以在控制器的任意业务方法里面使用这个实例变量。假设我们现在 从Spring的ApplicationContext或BeanFactory获得该控制器对象,我们就可以在 同一个上下文中配置一个LocalStatelessSessionProxyFactoryBean 的实例,它将作为EJB组件的代理对象。这个代理对象的配置和控制器的属性 myComponent的设置是使用一个配置项完成的,如下所示:

<bean id="myComponent"
      class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName"><value>myComponent</value></property>
    <property name="businessInterface"><value>com.mycom.MyComponent</value></property>
</bean>

<bean id="myController" class = "com.mycom.myController">
    <property name="myComponent"><ref bean="myComponent"/></property>
</bean>

这些看似简单的代码背后隐藏了很多复杂的处理,比如默默工作的Spring AOP框架,我们甚至不必知道这些概念,一样可以享用它的结果。Bean myComponent 的定义中创建了一个该EJB组件的代理对象,它实现了业务方法接口。这个EJB组件的 本地Home对象在启动的时候就被放到了缓存中,所以只需要执行一次JNDI查找即可。 每当EJB组件被调用的时候,这个代理对象就调用本地EJB组件的create方法,并调用 该EJB组件的相应的业务方法。

在Bean myController的定义中,控制器类的属性 myController的值被设置为上面代理对象。

这样的EJB组件访问方式大大简化了应用程序代码:Web层(或其他EJB客户端) 的代码不再依赖于EJB组件的使用。如果我们想把这个EJB的引用替换为一个POJO, 或者是模拟用的对象或其他测试组件,我们只需要简单地修改Bean myComponent 的定义中仅仅一行Java代码,此外,我们也不再需要在应用程序中编写任何JNDI查找 或其它EJB相关的代码。

评测和实际应用中的经验表明,这种方式的性能负荷极小,(尽管其中 使用了反射方式以调用目标EJB组件的方法),通常的使用中我们几乎觉察不出。请记住 我们并不想频繁地调用EJB组件的底层方法,虽然如此,有些性能代价是与应用服务器 中EJB的基础框架相关的。

关于JNDI查找有一点需要注意。在Bean容器中,这个类通常最好用作单件 (没理由使之成为原型)。不过,如果这个Bean容器会预先实例化单件(类似XML ApplicationContext的变体的行为),如果在EJB容器载入目标EJB前载入bean容器, 我们就可能会遇到问题。因为JNDI查找会在该类的init方法中被执行并且缓存结果, 这样就导致该EJB不能被绑定到目标位置。解决方案就是不要预先实例化这个工厂对象, 而允许它在第一次用到的时候再创建,在XML容器中,这是通过属性 lazy-init来控制的。

尽管大部分Spring的用户不会对这些感兴趣,但那些对EJB进行AOP的具体应用 的用户则会想看看LocalSlsbInvokerInterceptor

15.1.3. 访问远程的无状态Session Bean(SLSB)

基本上访问远程EJB与访问本地EJB差别不大,除了前者使用的是 SimpleRemoteStatelessSessionProxyFactoryBean。当然, 无论是否使用Spring,远程调用的语义都相同,不过,对于使用的场景和错误处理 来说,调用另外一台计算机上不同虚拟机中的对象的方法其处理有所不同。

与不使用Spring方式的EJB客户端相比,Spring的EJB客户端有一个额外的 好处。通常如果客户端代码随意在本地EJB和远程EJB的调用之间来回切换,就有 一个问题。这是因为远程接口的方法需要声明其会抛出RemoteException ,然后客户端代码必须处理这种异常,但是本地接口的方法却不需要这样。 如果要把针对本地EJB的代码改为访问远程EJB,就需要修改客户端代码,增加 对RemoteException的处理,反之就需要去掉这样的 异常处理。使用Spring 的远程EJB代理,我们就不再需要在业务方法接口和EJB的 代码实现中声明会抛出RemoteException,而是定义一个 相似的远程接口,唯一不同就是它抛出的是RemoteAccessException, 然后交给代理对象去动态的协调这两个接口。也就是说,客户端代码不再需要与 RemoteException这个显式(checked)异常打交道,实际运行中 所有抛出的异常RemoteException都会被捕获并转换成一个 隐式(non-checked)的RemoteAccessException,它是 RuntimeException的一个子类。这样目标服务端就可以 在本地EJB或远程EJB(甚至POJO)之间随意地切换,客户端不再需要关心甚至 根本不会觉察到这种切换。当然,这些都是可选的,我们并不阻止在业务接口中声明 异常RemoteExceptions

15.2. 使用Spring提供的辅助类实现EJB组件

Spring也提供了一些辅助类来为EJB组件的实现提供便利。它们是为了倡导一些 好的实践经验,比如把业务逻辑放在在EJB层之后的POJO中实现,只把事务隔离和 远程调用这些职责留给EJB。

要实现一个无状态或有状态的Session Bean,或消息驱动Bean,我们的实现 可以继承分别继承AbstractStatelessSessionBeanAbstractStatefulSessionBean,和 AbstractMessageDrivenBean/AbstractJmsMessageDrivenBean

考虑这个例子:我们把无状态Session Bean的实现委托给普通的Java服务对象。 业务接口的定义如下:

public interface MyComponent {
    public void myMethod(...);
    ...
}

这是简单Java对象实现方式的类:

public class MyComponentImpl implements MyComponent {
    public String myMethod(...) {
        ...
    }
    ...
}

最后是无状态Session Bean自身:

public class MyComponentEJB implements extends AbstractStatelessSessionBean
        implements MyComponent {

    MyComponent _myComp;

    /**
     * Obtain our POJO service object from the BeanFactory/ApplicationContext
     * @see org.springframework.ejb.support.AbstractStatelessSessionBean#onEjbCreate()
     */
    protected void onEjbCreate() throws CreateException {
        _myComp = (MyComponent) getBeanFactory().getBean(
            ServicesConstants.CONTEXT_MYCOMP_ID);
    }

    // for business method, delegate to POJO service impl.
    public String myMethod(...) {
        return _myComp.myMethod(...);
    }
    ...
}

Spring为支持EJB而提供的这些基类默认情况下会创建并载入一个BeanFactory (这个例子里,它是ApplicationContext的子类),作为其生命周期的一部分, 供EJB使用(比如像上面的代码那样用来获取POJO服务对象)。载入的工作是通过 一个策略对象完成的,它是BeanFactoryLocator的子类。 默认情况下,实际使用的BeanFactoryLocator的实现类是 ContextJndiBeanFactoryLocator,它根据一个JNDI环境变量 来创建一个ApplicationContext对象(这里是EJB类,路径是 java:comp/env/ejb/BeanFactoryPath)。如果需要改变 BeanFactory或ApplicationContext的载入策略,我们可以在子类中重定义了的 setSessionContext()方法或具体EJB子类的构造函数中调用 setBeanFactoryLocator()方法来改变默认使用的 BeanFactoryLocator实现类。具体细节请参考JavaDoc。

如JavaDoc中所述,有状态Session Bean在其生命周期中可能会被钝化并重新激活, 如果是不可序列化的BeanFactory或ApplicationContext,由于它们不会被EJB容器保存, 所以还需要手动在ejbPassivateejbActivate 这两个方法中分别调用unloadBeanFactory()loadBeanFactory, 才能在钝化或激活的时候卸载或载入。

有些情况下,要载入ApplicationContext以使用EJB组件, ContextJndiBeanFactoryLocator的默认实现基本上足够了, 不过,当ApplicationContext需要载入多个bean,或这些bean初始化所需的时间或内存 很多的时候(例如Hibernate的SessionFactory的初始化),就有可能出问题,因为 每个EJB组件都有自己的副本。这种情况下,用户会想重载 ContextJndiBeanFactoryLocator的默认实现,并使用其它 BeanFactoryLocator的变体,例如ContextSingleton 或者BeanFactoryLocator,他们可以载入并共享一个 BeanFactory或ApplicationContext来为多个EJB组件或其它客户端所公用。这样做 相当简单,只需要给EJB添加类似于如下的代码:

   /**
    * Override default BeanFactoryLocator implementation
    * 
    * @see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
    */
   public void setSessionContext(SessionContext sessionContext) {
       super.setSessionContext(sessionContext);
       setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
       setBeanFactoryLocatorKey(ServicesConstants.PRIMARY_CONTEXT_ID);
   }

请参考相应的JavaDoc来获取关于BeanFactoryLocatorContextSingleton以及BeanFactoryLocator 的用法的详细信息。

 

 

 

 

 

 

 

 

第 16 章 通过Spring使用远程访问和web服务

16.1. 简介

Spring提供类用于集成各种远程访问技术。这种对远程访问的支持可以降低你在用POJO实现支持远程访问业务时的开发难度。目前,Spring提供对下面四种远程访问技术的支持:

  • 远程方法调用(RMI)。通过使用RmiProxyFactoryBeanRmiServiceExporter,Spring支持传统的RMI(使用java.rmi.Remote interfaces 和 java.rmi.RemoteException)和通过RMI调用器(可以使用任何Java接口)的透明远程调用。

  • Spring的HTTP调用器。Spring提供一种特殊的远程调用策略支持任何Java接口(象RMI调用器一样),它允许Java序列化能够通过HTTP传送。对应的支持类是HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter

  • Hessian。通过使用HessianProxyFactoryBeanHessianServiceExporter,你可以使用Caucho提供的轻量级基于HTTP的二进制协议透明地提供你的业务。

  • Burlap。Burlap是基于XML的,它可以完全代替Hessian。Spring提供的支持类有BurlapProxyFactoryBeanBurlapServiceExporter

  • JAX RPC (TODO).

 

当讨论Spring对远程访问的支持时,我们将使用下面的域模型和对应的业务:

// Account domain object
public class Account implements Serializable{
  private String name;

  public String getName();
  public void setName(String name) {
    this.name = name;
  }
}
			

 

// Account service
public interface AccountService {

  public void insertAccount(Account acc);
  
  public List getAccounts(String name);
}
			

 

// ... and corresponding implement doing nothing at the moment
public class AccountServiceImpl implements AccountService {

  public void insertAccount(Account acc) {
    // do something
  }
  
  public List getAccounts(String name) {
    // do something
  }
}
			

 

我们先演示使用RMI向远程客户提供业务,并且会谈到使用RMI的缺点。然后我们将继续演示一个Hessian的例子。

16.2. 使用RMI提供业务

使用Spring的RMI支持,你可以透明地通过RMI提供你的业务。在配置好Spring的RMI支持后,你会看到一个和远程EJB类似的配置,除了没有对安全上下文传递和远程事务传递的标准支持。当使用RMI调用器时,Spring对这些额外的调用上下文提供捕获,所以你可以插入你的安全框架或安全信任逻辑。

16.2.1. 使用RmiServiceExporter提供业务

使用RmiServiceExporter,我们可以将AccountServer对象作为RMI对象输出接口。这个接口可以使用RmiProxyFactoryBean访问,或使用简单RMI把该接口当作传统RMI业务来访问。RmiServiceExporter支持通过RMI调用器提供任何非RMI业务。

当然,我们首先得在Spring的BeanFactory中设置我们的业务:

<bean id="accountService" class="example.AccountServiceImpl">
    <!-- any additional properties, maybe a DAO? -->
</bean>
				

 

接下来,我们使用RmiServiceExporter提供我们的业务:

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
	<!-- does not necessarily have to be the same name as the bean to be exported -->
	<property name="serviceName"><value>AccountService</value></property>
	<property name="service"><ref bean="accountService"/></property>
	<property name="serviceInterface"><value>example.AccountService</value></property>
	<!-- defaults to 1099 -->
	<property name="registryPort"><value>1199</value></property>
</bean>
				

正如你看到的,我们更换了RMI注册的端口。通常,你的应用服务器会维护RMI注册,我们最好不要干扰它。业务名被用来绑定业务。所以现在,业务就绑定在rmi://HOST:1199/AccountService上。我们将在客户端使用URL来连接业务。

注意:我们漏了一个属性,就是servicePort属性,它缺省值为0。这个意味着该业务使用匿名端口通讯。当然你也可以指定一个端口。

16.2.2. 客户端连接业务

我们的客户端是一个使用AccountService管理账户的简单对象:

public class SimpleObject {
  private AccountService accountService;
  public void setAccountService(AccountService accountService) {
    this.accountService = accountService;
  }
}
				

 

为了在客户端连接业务,我们建立另一个bean工厂,它包含这个简单对象和业务连接的配置信息:

<bean class="example.SimpleObject">
	<property name="accountService"><ref bean="accountService"/></bean>
</bean>

<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
	<property name="serviceUrl"><value>rmi://HOST:1199/AccountService</value></property>
	<property name="serviceInterface"><value>example.AccountService</value></property>
</bean>
				

这就是我们在客户端访问远程账户业务所需要做的。Spring透明地创建一个调用器,通过RmiServiceExporter远程提供账户业务。在客户端,我们使用RmiProxyFactoryBean来使用该业务。

16.3. 使用Hessian或Burlap通过HTTP远程调用业务

Hessian提供了一个基于HTTP的二进制远程协议。它由Caucho创建,更多有关Hessian的信息可以访问http://www.caucho.com

16.3.1. 为Hessian建立DispatcherServlet

Hessian使用一个特定的servlet来通过HTTP通讯。使用Spring的DispatcherServlet概念,你可以很容易地创建这样的servlet来提供你的业务。首先我们必须在你的应用中创建一个新的servlet(下面来自web.xml):

<servlet>
	<servlet-name>remote</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
				

 

你可能熟悉Spring的DispatcherServlet概念,如果是的话,你得在WEB-INF目录下建立一个应用上下文,remote-servlet.xml 。这个应用上下文会在下一节中使用。

16.3.2. 使用HessianServiceExporter提供你的bean

在这个新的应用上下文remote-servlet.xml中,我们将创建一个HessianServiceExporter来输出你的业务:

<bean id="accountService" class="example.AccountServiceImpl">
    <!-- any additional properties, maybe a DAO? -->
</bean>

<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
    <property name="service"><ref bean="accountService"/></property>
    <property name="serviceInterface">
        <value>example.AccountService</value>
    </property>
</bean>
				

现在我们准备在客户端连接这个业务。我们使用BeanNameUrlHandlerMapping,就不需要指定处理器映射将请求(url)映射到业务上,因此业务提供在http://HOST:8080/AccountService上。

16.3.3. 客户端连接业务

我们在客户端使用HessianProxyFactoryBean来连接业务。和RMI例子中的原则一样。我们将创建一个单独的bean工厂或应用上下文,在SimpleObject使用AccountService来管理账户的地方将会提到下列bean:

<bean class="example.SimpleObject">
    <property name="accountService"><ref bean="accountService"/></property>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
	<property name="serviceUrl"><value>http://remotehost:8080/AccountService</value></property>
	<property name="ServiceInterface"><value>example.AccountService</value></property>
</bean>
				

就是这样简单。

16.3.4. 使用Burlap

我们不在这里讨论Burlap,它只不过是Hessian的基于XML实现。因为它和上面的Hessian的例子以相同的方式配置。只要你把Hessian替换成Burlap就可以了。

16.3.5. 在通过Hessian或Burlap输出的业务中应用HTTP基本认证

Hessian和Burlap的优点之一就是我们能很容易地应用HTTP认证,因为两者都是基于HTTP的协议。例如,普通的HTTP服务器安全机制可以很容易地通过使用web.xml安全功能来应用。通常,你不会为每个用户都建立不同的安全信任,而是在Hessian/Burlap的ProxyFactoryBean中定义可共享的信任(和JDBC DataSource相类似)。

 

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
	<property name="interceptors">
		<list>
			<ref bean="authorizationInterceptor"/>
		</list>
	</property>
</bean>

<bean id="authorizationInterceptor" 
	class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
	<property name="authorizedRoles">
		<list>
			<value>administrator</value>
			<value>operator</value>
		</list>
	</property>	
</bean>
				

 

这个例子中,我们用到了BeanNameUrlHandlerMapping,并设置了一个拦截器,它只允许管理员和操作员调用这个应用上下文中的bean。

注意:当然,这个例子并没有演示灵活的安全设施。如果考虑更灵活的安全设置,可以去看看Acegi Security System,http://acegisecurity.sourceforge.net

16.4. 使用HTTP调用器输出业务

和Burlap和Hessian使用自身序列化机制的轻量级协议相反,Spring HTTP调用器使用标准Java序列化机制来通过HTTP输出业务。如果你的参数或返回值是复杂类型,并且不能通过Hessian和Burlap的序列化机制序列化,HTTP调用器就很有优势(参阅下一节,选择远程技术时的考虑)。

实际上,Spring可以使用J2SE提供的标准功能或Commons的HttpClient来实现HTTP调用。如果你需要更先进,更好用的功能,就使用后者。你可以参考jakarta.apache.org/commons/httpclient

16.4.1. 输出业务对象

为业务对象设置HTTP调用器和你在Hessian或Burlap中使用的方式类似。就象Hessian提供HessianServiceExporter,Spring的HTTP调用器提供了org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter。为了输出AccountService,使用下面的配置:

    <bean name="/AccountService" class="org.sprfr.remoting.httpinvoker.HttpInvokerServiceExporter">
        <property name="service"><ref bean="accountService"/></property>
        <property name="serviceInterface">
            <value>example.AccountService</value>
        </property>
	</bean>

 

16.4.2. 在客户端连接业务

同样,从客户端连接业务与你使用Hessian或Burlap时做的类似。使用代理,Spring可以将你的调用翻译成HTTP 的POST请求到指向输出业务的URL。

	
	<bean id="httpInvokerProxy" class="org.sprfr.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
		<property name="serviceUrl">
			<value>http://remotehost:8080/AccountService</value>
		</property>
		<property name="serviceInterface">
			<value>example.AccountService</value>
		</property>
	</bean>

 

就象上面说的一样,你可以选择使用你想使用的HTTP客户端。缺省情况下,HttpInvokerPropxy使用J2SE的HTTP功能,但是你也可以通过设置httpInvokerRequestExecutor属性选择使用Commons HttpClient:

<property name="httpInvokerRequestExecutor">
	<bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>

 

16.5. 在选择这些技术时的一些考虑

这里提到的每种技术都有它的缺点。你在选择这些技术时,应该仔细考虑你的需要,你所输出的业务和你在远程访问时传送的对象。

当使用RMI时,通过HTTP协议访问对象是不可能的,除非你用HTTP包裹RMI流。RMI是一种很重的协议,因为他支持完全的对象序列化,这样的序列化在要求复杂数据结构在远程传输时是非常重要的。然而,RMI-JRMP只能绑定到Java客户端:它是一种Java-to-Java的远程访问的方案。

如果你需要基于HTTP的远程访问而且还要求使用Java序列化,Spring的HTTP调用器是一个很好的选择。它和RMI调用器使用相同的基础设施,仅仅使用HTTP作为传输方式。注意HTTP调用器不仅只能用在Java-to-Java的远程访问,而且在客户端和服务器端都必须使用Spring。(Spring为非RMI接口提供的RMI调用器也要求客户端和服务器端都使用Spring)

当在异构环境中,Hessian和Burlap就非常有用了。因为它们可以使用在非Java的客户端。然而,对非Java支持仍然是有限制的。已知的问题包括含有延迟初始化的collection对象的Hibernate对象的序列化。如果你有一个这样的数据结构,考虑使用RMI或HTTP调用器,而不是Hessian。

最后但也很重要的一点,EJB优于RMI,因为它支持标准的基于角色的认证和授权,以及远程事务传递。用RMI调用器或HTTP调用器来支持安全上下文的传递是可能的,虽然这不是由核心Spring提供:而是由第三方或在定制的解决方案中插入拦截器来解决的。

 

 

 

 

 

 

 

 

 

 

 

 

第 17 章 使用Spring邮件抽象层发送Email

17.1. 简介

Spring提供了一个发送电子邮件的高级抽象层,它向用户屏蔽了底层邮件系统的一些细节,同时负责低层次的代表客户端的资源处理。

17.2. Spring邮件抽象结构

Spring邮件抽象层的主要包为org.springframework.mail。它包括了发送电子邮件的主要接口MailSender和 封装了简单邮件的属性如from, to,cc, subject, text值对象叫做SimpleMailMessage。 一个以MailException为root的checked Exception继承树,它们提供了对底层邮件系统异常的高级别抽象。 请参考JavaDocs来得到关于邮件异常层次的更多的信息。

为了使用JavaMail中的一些特色如MIME类型的消息,Spring也提供了一个MailSender的子接口, 名为org.springframework.mail.javamail.JavaMailSender,同时也提供了一个对JavaMail的MIME类型的消息分块的回调interface, 名为org.springframework.mail.javamail.MimeMessagePreparator

MailSender:

public interface MailSender {

    /**
     * Send the given simple mail message.
     * @param simpleMessage message to send
     * @throws MailException in case of message, authentication, or send errors
     */
    public void send(SimpleMailMessage simpleMessage) throws MailException;

    /**
     * Send the given array of simple mail messages in batch.
     * @param simpleMessages messages to send
     * @throws MailException in case of message, authentication, or send errors
     */
    public void send(SimpleMailMessage[] simpleMessages) throws MailException;

}

JavaMailSender:

public interface JavaMailSender extends MailSender {

    /**
     * Create a new JavaMail MimeMessage for the underlying JavaMail Session
     * of this sender. Needs to be called to create MimeMessage instances
     * that can be prepared by the client and passed to send(MimeMessage).
     * @return the new MimeMessage instance
     * @see #send(MimeMessage)
     * @see #send(MimeMessage[])
     */
    public MimeMessage createMimeMessage();

    /**
     * Send the given JavaMail MIME message.
     * The message needs to have been created with createMimeMessage.
     * @param mimeMessage message to send
     * @throws MailException in case of message, authentication, or send errors
     * @see #createMimeMessage
     */
    public void send(MimeMessage mimeMessage) throws MailException;

    /**
     * Send the given array of JavaMail MIME messages in batch.
     * The messages need to have been created with createMimeMessage.
     * @param mimeMessages messages to send
     * @throws MailException in case of message, authentication, or send errors
     * @see #createMimeMessage
     */
    public void send(MimeMessage[] mimeMessages) throws MailException;

    /**
     * Send the JavaMail MIME message prepared by the given MimeMessagePreparator.
     * Alternative way to prepare MimeMessage instances, instead of createMimeMessage
     * and send(MimeMessage) calls. Takes care of proper exception conversion.
     * @param mimeMessagePreparator the preparator to use
     * @throws MailException in case of message, authentication, or send errors
     */
    public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException;

    /**
     * Send the JavaMail MIME messages prepared by the given MimeMessagePreparators.
     * Alternative way to prepare MimeMessage instances, instead of createMimeMessage
     * and send(MimeMessage[]) calls. Takes care of proper exception conversion.
     * @param mimeMessagePreparators the preparator to use
     * @throws MailException in case of message, authentication, or send errors
     */
    public void send(MimeMessagePreparator[] mimeMessagePreparators) throws MailException;

}

MimeMessagePreparator:

public interface MimeMessagePreparator {

    /**
     * Prepare the given new MimeMessage instance.
     * @param mimeMessage the message to prepare
     * @throws MessagingException passing any exceptions thrown by MimeMessage
     * methods through for automatic conversion to the MailException hierarchy
     */
    void prepare(MimeMessage mimeMessage) throws MessagingException;

}

17.3. 使用Spring邮件抽象

让我们来假设有一个业务接口名为OrderManager

public interface OrderManager {

    void placeOrder(Order order);
}

同时有一个use case为:需要生成带有订单号的email信息,并向客户发送该订单。 为了这个目的我们会使用MailSenderSimpleMailMessage

请注意,通常情况下,我们在业务代码中使用接口而让Spring ioc容器负责组装我们需要的合作者。

这里为OrderManager的一个实现

import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

public class OrderManagerImpl implements OrderManager {

    private MailSender mailSender;
    private SimpleMailMessage message;

    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void setMessage(SimpleMailMessage message) {
        this.message = message;
    }

    public void placeOrder(Order order) {

        //... * Do the businness calculations....
        //... * Call the collaborators to persist the order

        //Create a threadsafe "sandbox" of the message
        SimpleMailMessage msg = new SimpleMailMessage(this.message);
        msg.setTo(order.getCustomer().getEmailAddress());
        msg.setText(
            "Dear "
                + order.getCustomer().getFirstName()
                + order.getCustomer().getLastName()
                + ", thank you for placing order. Your order number is "
                + order.getOrderNumber());
        try{
            mailSender.send(msg);
        }
        catch(MailException ex) {
            //log it and go on
            System.err.println(ex.getMessage());            
        }
    }
}

上面的代码的bean的定义应该是这样的:

<bean id="mailSender"
      class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host"><value>mail.mycompany.com</value></property>
</bean>

<bean id="mailMessage"
      class="org.springframework.mail.SimpleMailMessage">
    <property name="from"><value>customerservice@mycompany.com</value></property>
    <property name="subject"><value>Your order</value></property>
</bean>

<bean id="orderManager"
      class="com.mycompany.businessapp.support.OrderManagerImpl">
    <property name="mailSender"><ref bean="mailSender"/></property>
    <property name="message"><ref bean="mailMessage"/></property>
</bean>

下面是OrderManager的实现,使用了MimeMessagePreparator回调接口。 请注意这里的mailSender属性类型为JavaMailSender,这样做是为了能够使用JavaMail的MimeMessage:

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import javax.mail.internet.MimeMessage;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessagePreparator;

public class OrderManagerImpl implements OrderManager {
    private JavaMailSender mailSender;
    
    public void setMailSender(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void placeOrder(final Order order) {

        //... * Do the businness calculations....
        //... * Call the collaborators to persist the order
        
        
        MimeMessagePreparator preparator = new MimeMessagePreparator() {
            public void prepare(MimeMessage mimeMessage) throws MessagingException {
                mimeMessage.setRecipient(Message.RecipientType.TO, 
                        new InternetAddress(order.getCustomer().getEmailAddress()));
                mimeMessage.setFrom(new InternetAddress("mail@mycompany.com"));
                mimeMessage.setText(
                    "Dear "
                        + order.getCustomer().getFirstName()
                        + order.getCustomer().getLastName()
                        + ", thank you for placing order. Your order number is "
                        + order.getOrderNumber());
            }
        };
        try{
            mailSender.send(preparator);
        }
        catch(MailException ex) {
            //log it and go on
            System.err.println(ex.getMessage());            
        }
    }
}

如果你想使用JavaMail MimeMessage以获得全部的能力,只需要你指尖轻触键盘即可使用MimeMessagePreparator

请注意这部分邮件代码是一个横切关注点,是一个可以重构至一个定制的SpringAOP advice的完美候选者, 这样就可以不费力的应用到目标对象OrderManager上来。关于这一点请看AOP章节。

17.3.1. 可插拔的MailSender实现

Spring提供两种MailSender的实现:标准的JavaMail实现和在http://servlets.com/cos (com.oreilly.servlet)里的Jason Hunter's MailMessage类之上的实现。请参考JavaDocs以得到进一步的信息。

 

 

 

 

 

 

 

第 18 章 使用Quartz或Timer完成时序调度工作

18.1. 简介

Spring提供了支持时序调度(译者注:Scheduling,下同)的整合类.现在, Spring支持内置于1.3版本以来的JDK中的Timer和Quartz Scheduler(http://www.quartzscheduler.org)。 两个时序调度器通过FactoryBean建立,保持着可选的对Timers或者Triggers的引用。更进一步的, 对于Quartz Scheduler和Timer两者存在一个方便的类允许你调用目标对象(类似于通常的MethodInvokingFactoryBeans)上的某个方法

18.2. 使用OpenSymphony Quartz Scheduler

Quartz使用Triggers,JobsJobDetail来实现时序调度中的各种工作。 为了了解Quartz背后的种种基本观点,你可以移步至http://www.opensymphony.com/quartz。 为了方便的使用,Spring提供了几个类在基于Spring的应用中来简化对Quartz的使用。

18.2.1. 使用JobDetailBean

JobDetail 对象包括了运行一个job所需要的所有信息。 于是Spring提供了一个所谓的JobDetailBean使得JobDetail拥有了一个真实的,有意义的默认值。让我们来看个例子:

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailBean">
  <property name="jobClass">
    <value>example.ExampleJob</value>
  </property>
  <property name="jobDataAsMap">
    <map>
      <entry key="timeout"><value>5</value></entry>
    </map>
  </property>
</bean>
			

Job detail bean拥有所有运行job(ExampleJob)的必要信息。通过job的data map来制定timeout。 Job的data map可以通过JobExecutionContext(在运行时刻传递给你)来得到, 但是JobDetailBean也把从job的data map中得到的属性映射到实际job中的属性中去。 所以,如果ExampleJob中包含一个名为timeout的属性,JobDetailBean将自动为它赋值:

package example;

public class ExampleJob extends QuartzJobBean {

  private int timeout;
  
  /**
   * Setter called after the ExampleJob is instantiated
   * with the value from the JobDetailBean (5)
   */ 
  public void setTimeout(int timeout) {
    this.timeout = timeout;
  }
  
  protected void executeInternal(JobExecutionContext ctx)
  throws JobExecutionException {
  
      // do the actual work
      
  }
}
			

所有Job detail bean中的一些其他的设定对你来说也是可以同样设置的.

注意:使用namegroup属性,你可以修改job在哪一个组下运行和使用什么名称。 默认情况下,job的名称等于job detai bean的名称(在上面的例子中为exampleJob)。

18.2.2. 使用MethodInvokingJobDetailFactoryBean

通常情况下,你只需要调用特定对象上的一个方法。你可以使用MethodInvokingJobDetailFactoryBean准确的做到这一点:

<bean id="methodInvokingJobDetail" 
  class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject"><ref bean="exampleBusinessObject"/></property>
    <property name="targetMethod"><value>doIt</value></property>
</bean>

上面例子将导致exampleBusinessObject中的doIt方法被调用(如下):

public class BusinessObject {
  
  // properties and collaborators
  
  public void doIt() {
    // do the actual work
  }
}
			

 

<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
			

使用MethodInvokingJobDetailFactoryBean你不需要创建只有一行代码且只调用一个方法的job, 你只需要创建真实的业务对象来包装具体的细节的对象。

默认情况下,Quartz Jobs是无状态的,可能导致jobs之间互相的影响。如果你为相同的JobDetail指定两个触发器, 很可能当第一个job完成之前,第二个job就开始了。如果JobDetail对象实现了Stateful接口,就不会发生这样的事情。 第二个job将不会在第一个job完成之前开始。为了使得jobs不并发运行,设置MethodInvokingJobDetailFactoryBean中的concurrent标记为false

<bean id="methodInvokingJobDetail" 
  class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject"><ref bean="exampleBusinessObject"/></property>
    <property name="targetMethod"><value>doIt</value></property>
</bean>			
			

注意:默认情况下,jobs在并行的方式下运行。

18.2.3. 使用triggers和SchedulerFactoryBean来包装任务

我们已经创建了job details,jobs。我们回顾了允许你调用特定对象上某一个方法的便捷的bean。 当然我们仍需要调度这些jobs。这需要使用triggers和SchedulerFactoryBean来完成。 Quartz自带一些可供使用的triggers。Spring提供两个子类triggers,分别为CronTriggerBeanSimpleTriggerBean

Triggers也需要被调度。Spring提供SchedulerFactoryBean来暴露一些属性来设置triggers。SchedulerFactoryBean负责调度那些实际的triggers。

两个例子:

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
  <property name="jobDetail">
    <!-- see the example of method invoking job above -->    
    <ref bean="methodInvokingJobDetail"/>
  </property>
  <property name="startDelay">
    <!-- 10 seconds -->
    <value>10000</value>
  </property>
  <property name="repeatInterval">
    <!-- repeat every 50 seconds -->
    <value>50000</value>
  </property>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
  <property name="jobDetail">
    <ref bean="exampleJob"/>
  </property>
  <property name="cronExpression">
    <!-- run every morning at 6 am -->
    <value>0 6 * * 1</value>
  </property>
</bean>
			

现在我们创建了两个triggers,其中一个开始延迟10秒以后每50秒运行一次,另一个每天早上6点钟运行。 我们需要创建一个SchedulerFactoryBean来最终实现上述的一切:

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="triggers">
    <list>
      <ref local="cronTrigger"/>
      <ref local="simpleTrigger"/>
    </list>
  </property>
</bean>
			

更多的一些属性你可以通过SchedulerFactoryBean来设置,例如job details使用的Calendars,用来订制Quartz的一些属性以及其它。 你可以看相应的JavaDOC(http://www.springframework.org/docs/api/org/springframework/scheduling/quartz/SchedulerFactoryBean.html)来了解进一步的信息。

18.3. 使用JDK Timer支持类

另外一个调度任务的途径是使用JDK Timer对象。更多的关于Timers的信息可以在这里http://java.sun.com/docs/books/tutorial/essential/threads/timer.html找到。 上面讨论的概念仍可以应用于Timer的支持。你可以创建定制的timer或者调用某些方法的timer。 包装timers的工作由TimerFactoryBean完成。

18.3.1. 创建定制的timers

你可以使用TimerTask创建定制的timer tasks,类似于Quartz中的jobs:

public class CheckEmailAddresses extends TimerTask {

  private List emailAddresses;
  
  public void setEmailAddresses(List emailAddresses) {
    this.emailAddresses = emailAddresses;
  }
  
  public void run() {
  
    // iterate over all email addresses and archive them
    
  }
}
			

包装它是简单的:

<bean id="checkEmail" class="examples.CheckEmailAddress">
  <property name="emailAddresses">
    <list>
      <value>test@springframework.org</value>
      <value>foo@bar.com</value>
      <value>john@doe.net</value>
    </list>
  </property>
</bean>

<bean id="scheduledTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
  <!-- wait 10 seconds before starting repeated execution -->
  <property name="delay">
    <value>10000</value>
  </property>
  <!-- run every 50 seconds -->
  <property name="period">
    <value>50000</value>
  </property>
  <property name="timerTask">
    <ref local="checkEmail"/>
  </property>
</bean>
			

 

18.3.2. 使用MethodInvokingTimerTaskFactoryBean

就像Quartz的支持一样,Timer的支持也有一个组件允许你周期性地调用一个方法:

<bean id="methodInvokingTask" 
  class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean">
    <property name="targetObject"><ref bean="exampleBusinessObject"/></property>
    <property name="targetMethod"><value>doIt</value></property>
</bean>

上面的例子将会导致exampleBusinessObject上的doIt方法被调用(如下):

public class BusinessObject {
  
  // properties and collaborators
  
  public void doIt() {
    // do the actual work
  }
}
			

把上面例子中提到ScheduledTimerTask的引用改为methodInvokingTask将导致该task被执行。

18.3.3. 包装:使用TimerFactoryBean来建立tasks

TimerFactoryBean类似于QuartzSchedulerFactoryBean,都是服务于一个目的:建立起实际的时序调度。 TimerFactoryBean建立一个实际的Timer来调度它引用的那些tasks。你可以指定它是否使用一个守护线程。

<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
  <property name="scheduledTimerTasks">
    <list>
      <!-- see the example above -->
      <ref local="scheduledTask"/>
    </list>
  </property>
</bean>
			

就是这些了!

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics