第 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栈上自始至终使用反向控制。