Chapter 15. 集成其它Web框架

15.1. 简介

本章将详细介绍Spring如何与 StrutsJSFTapestry以及 WebWork 这样的 第三方框架集成。

Spring 框架最具核心价值的一个提议就是允许 选择。总的来说, Spring 不会强迫大家去使用或者是购买任何特定的架构,技术或者开发方法(虽然它肯定会有倾向性的推荐一些)。 选择架构、技术、开发方法的自由是与开发人员以及他(她)所在的开发团队戚戚相关的, 这在 Web 领域是个不争的事实。Spring 提供了自己的 Web 框架(SpringMVC), 同时它也提供了与其它流行的 Web 框架整合的能力。这就允许开发人员充分利用已经掌握的技术, 比如某人可以使用他所熟悉的 Struts 框架,同时他也可以享受 Spring 提供的其他功能, 例如数据访问,声明式事务处理,以及灵活的配置和方便的应用集成。

上一段简单介绍了Spring的一些卖点,这章剩下的部分将集中介绍如何用 Spring 集成你所喜欢的 Web 框架。 那些从其他语言转向 Java 的开发者们经常说,Java 里面的 Web 框架是在太多了...事实的确如此; 这也意味着在一个章节里想要涵盖所有框架的细节是绝对不可能的。这一章选择了 Java 中四个最流行的 Web 框架, 首先介绍对于所有框架都适用的 Spring 配置,然后对每个支持的 Web 框架提供详细的集成选项。

请注意这一章并不解释如何使用某种特定的 Web 框架。举个例子,如果你想要使用 Struts 作为 Web 应用的表现层,在阅读本章以前,你应该已经熟悉了 Struts。 如果你想要了解那些 Web 框架的详细信息, 请参考本章的结尾:Section 15.7, “更多资源”

15.2. 通用配置

在深入研究如何集成受支持的 Web 框架之前,让我们先看看对所有 Web 框架都适用的 Spring 配置。(这一节同样适用于 Spring 自己的 Web 框架,SpringMVC)。

在 Spring 所支持的轻量级应用模型中,有一个概念叫“分层架构”。在经典的分层架构中, Web 层只是很多层中的一层...它是服务器端应用的一个入口,它将请求委派给定义在服务层的服务对象 (门面)以满足业务用例需求(这些是表现层技术触及不到的)。在 Spring 中,这些服务对象,以及其他的业务对象, 数据访问对象等等,都存在于一个独立的“businnes context”中,这个context含有任何 Web 或者表现层的对象(表现层对象诸如 Spring MVC 控制器通常被配置于一个独立的“presentation context”中)。 这一节详细介绍在一个应用中如何配置一个 Spring 容器(WebApplicationContext)来容纳所有的“business beans”。

现在进入细节部分...所有你需要做的就是在 Web 应用的 web.xml 文件中声明一个 ContextLoaderListener 并且在同一文件里增加一个 contextConfigLocation <context-param/> , 这个声明决定了哪些 Spring XML 配置文件将要被加载。

以下是 <listener/> 的配置:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
[Note]Note

Listener 是在 Servlet API 2.3 版本中才加入的。如果你使用只支持 Servlet 2.2 版本的容器, 你可以使用 ContextLoaderServlet 完成相同的功能。

以下是 <context-param/> 的配置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

如果你没有指定 contextConfigLocation 的context参数, ContextLoaderListener 将会寻找一个名为 /WEB-INF/applicationContext.xml 的文件以加载。 一旦context文件被加载,Spring 通过文件中 bean 的定义创建一个 WebApplicationContext 对象并且将它储存在 Web 应用的 ServletContext 中。

所有 Java Web 框架都构建在 Servlet API 之上,所以可以使用下面的代码片断访问这个 由 ContextLoaderListener 创建的ApplicationContext

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 这个类提供了方便的功能,这样你就不必去记 ServletContext 中属性的名字。 它的 getWebApplicationContext() 方法在 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 这个键值不对应任何对象的时候将返回 null。 不过,为了避免在应用中得到 NullPointerExceptions ,我们推荐你使用 getRequiredWebApplicationContext() 方法。 这个方法在ApplicationContext 缺失的时候会抛出一个异常。

一旦你获得了一个 WebApplicationContext 的引用, 你可以通过 bean 的名字或类型来获得它们。大多数开发人员通过名字获得 bean, 然后将它们转换成相应的接口类型。

幸运的是,这一节中的大多数框架都有更简单的方法来查询 bean。我们不仅仅可以更简单地从 Spring 容器中 得到 bean,我们还可以在控制器中使用 Spring 依赖注入的特性。下面的几个小节是每种框架集成策略的详细描述。

15.3. JavaServer Faces

JavaServer Faces (JSF) 是一个基于组件的,事件驱动的 Web 框架。这个框架很受欢迎。 Spring 与 JSF 集成的关键类是 DelegatingVariableResolver

15.3.1. DelegatingVariableResolver

将 Spring 中间层与 JSF Web 层整合的最简单办法就是使用 DelegatingVariableResolver 类。 要在应用中配置变量解析器(Variable Resolver),你需要编辑 faces-context.xml 文件。 在 <faces-config/> 元素里面增加一个 <application/> 元素和一个 <variable-resolver/> 元素。 变量解析器的值将引用 Spring 的 DelegatingVariableResolver。例如:

<faces-config>
  <application>
    <variable-resolver>org.springframework.web.jsf.DelegatingVariableResolver</variable-resolver>
      <locale-config>
        <default-locale>en</default-locale>
        <supported-locale>en</supported-locale>
        <supported-locale>es</supported-locale>
      </locale-config>
      <message-bundle>messages</message-bundle>
    </application>
</faces-config>

DelegatingVariableResolver 首先会将查询请求委派到 JSF 实现的 默认的解析器中,然后才是 Spring 的“business context” WebApplicationContext。 这使得在JSF 所管理的 bean 中使用依赖注射非常容易。

JSF 所管理的 bean 都定义在 faces-config.xml 文件中。 下面例子中的 #{userManager} 是一个取自 Spring 的“business context”的 bean。

<managed-bean>
  <managed-bean-name>userList</managed-bean-name>
	<managed-bean-class>com.whatever.jsf.UserList</managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
    <property-name>userManager</property-name>
    <value>#{userManager}</value>
  </managed-property>
</managed-bean>

15.3.2. FacesContextUtils

如果所有属性已经映射到 faces-config.xml 文件中相关的bean, 一个自定义的 VariableResolver 也可以工作的很好。 但是有些情况下你需要显式获取一个bean。这时, FacesContextUtils 可以使这个任务变得很容易。它类似于 WebApplicationContextUtils, 不过它接受 FacesContext 而不是 ServletContext 作为参数。

ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());

我们推荐使用 DelegatingVariableResolver 实现 JSF 和 Spring 的集成。 如果你想要更全面的集成,可以看看 JSF-Spring 这个项目。

15.4. Struts

Struts应用最广的 Java Web 开发框架,主要是因为它是最先发行的几个框架之一(2001年6月)。这个框架由 Craig McClanahan 开发完成,现在作为 Apache 软件基金会的一个开源项目。 当时,它极大地简化了 JSP/Servlet 编程范例并且赢得了 大多数正在使用私人框架的开发人员的亲睐。它简化了编程模型,它是开源的,它具有一个 庞大的社区,这些都使得这个项目快速成长,同时变得越来越流行。

要将 Struts 与 Spring 集成,你有两个选择:

  • 配置 Spring 将 Action 作为 bean 托管,使用 ContextLoaderPlugin, 并且在 Spring context中设置依赖关系。

  • 继承 Spring 的 ActionSupport 类并且 使用getWebApplicationContext() 方法获取 Spring 管理的 bean。

15.4.1. ContextLoaderPlugin

ContextLoaderPlugin 是 Struts 1.1+ 的插件,用来为 Struts 的 ActionServlet 加载 Spring context文件。 这个context引用 WebApplicationContext (由 ContextLoaderListener 加载) 作为它的父类。默认的context文件是映射的 Servlet 的名字,加上 -servlet.xml后缀。 如果 ActionServlet 在 web.xml 里面的定义是 <servlet-name>action</servlet-name>, 那么默认的文件就是 /WEB-INF/action-servlet.xml

要配置这个插件,请把下面的 XML 贴到 struts-config.xml 文件中 plug-ins 部分的底端:

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"/>

context配置文件的位置可以通过 contextConfigLocation属性来自定义。

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
  <set-property property="contextConfigLocation"
      value="/WEB-INF/action-servlet.xml.xml,/WEB-INF/applicationContext.xml"/>
</plug-in>

你也可以使用这个插件加载所有的配置文件,这在使用测试工具(例如 StrutsTestCase)的时候特别有用。 StrutsTestCase 的 MockStrutsTestCase 不会在启动的时候初始化 Listener, 将你所有的配置文件放在plug-in里面是一种解决方案。(有个 已记录的 bug 就是针对这个问题的,但是已经被标记为“无须改正”)。

struts-config.xml 中配置好插件以后,你可以配置Sping来管理 Action。Spring (1.1.3以后的版本) 提供下面两种方式:

  • 用 Spring 的DelegatingRequestProcessor重载 Struts 默认的 RequestProcessor

  • <action-mapping>type 属性设为 DelegatingActionProxy

这两种方法都允许你在 action-context.xml 文件中管理你的 Action 以及依赖关系。 连接 struts-config.xmlaction-servlet.xml 中的 Action 的桥梁 是 action-mapping 的“path”和 bean 的“name”。如果你在 struts-config.xml 文件中有如下配置:

<action path="/users" .../>

你必须在 action-servlet.xml 中将 Action bean 的名字定义为 “/users”:

<bean name="/users" .../>

15.4.1.1. DelegatingRequestProcessor

为了在 struts-config.xml 文件中配置 DelegatingRequestProcessor,你需要重载 <controller> 元素的 “processorClass” 属性。 下面的几行应该放在 <action-mapping> 元素的后面。

<controller>
  <set-property property="processorClass"
      value="org.springframework.web.struts.DelegatingRequestProcessor"/>
</controller>

增加这些设置之后,不管你查询任何类型的 Action,Sping都自动在它的context配置文件中寻找。 实际上,你甚至不需要指定类型。下面两个代码片断都可以工作:

<action path="/user" type="com.whatever.struts.UserAction"/>		
<action path="/user"/>

如果你使用 Struts 的 modules 特性,你的 bean 命名必须含有 module 的前缀。 举个例子,如果一个 Action 的定义为 <action path="/user"/>,而且它的 module 前缀为“admin”, 那么它应该对应名为 <bean name="/admin/user"/> 的 bean。

[Note]Note

如果你在 Struts 应用中使用了 Tiles,你需要配置 <controller> 为 DelegatingTilesRequestProcessor

15.4.1.2. DelegatingActionProxy

如果你有一个自定义的 RequestProcessor 并且不能够使用 DelegatingRequestProcessor 或者 DelegatingTilesRequestProcessor,你可以使用 DelegatingActionProxy 作为你 action-mapping 中的类型。

<action path="/user" type="org.springframework.web.struts.DelegatingActionProxy"
    name="userForm" scope="request" validate="false" parameter="method">
  <forward name="list" path="/userList.jsp"/>
  <forward name="edit" path="/userForm.jsp"/>
</action>

action-servlet.xml 文件中的bean定义依然不变,不管你使用了自定义的 RequestProcessor 还是 DelegatingActionProxy

如果你把 Action 定义在Spring的context文件里,那么 Spring bean 容器的所有特性都可用了: 比如,依赖注入,再比如,为每个请求初始化一个新的 Action 实例。 如果要使用这个特性, Action bean 定义中需要声明scope="prototype"

<bean name="/user" scope="prototype" autowire="byName"
    class="org.example.web.UserAction"/>

15.4.2. ActionSupport

正如前面提到的,你可以使用 WebApplicationContextUtils 类从 ServletContext 中获得 WebApplicationContext 。 另一个简单的办法是继承 Spring 的 Action 类。举个例子,除了继承 Struts 的 Action 之外,你也可以继承 Spring 的 ActionSupport 类。

ActionSupport 类提供了一些便利的方法,例如 getWebApplicationContext()。 下面的例子展示了如何在 Action 中使用它:

public class UserAction extends DispatchActionSupport {

    public ActionForward execute(ActionMapping mapping,
                                 ActionForm form,
                                 HttpServletRequest request,
                                 HttpServletResponse response) throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("entering 'delete' method...");
        }
        WebApplicationContext ctx = getWebApplicationContext();
        UserManager mgr = (UserManager) ctx.getBean("userManager");
        // talk to manager for business logic
        return mapping.findForward("success");
    }
}

Spring 包含了所有标准 Struts Action 的子类 - Spring 版本在类名末尾附加了 Support

你应该选择最适合你项目的集成方式。继承使得你的代码更可靠,并且你确切地知道依赖关系是如何被解析的。 另一方面,使用 ContextLoaderPlugin 允许你方便地在context XML 文件里面增加新的 依赖关系。这两种集成方法,不管哪一种 Spring 都提供了相当好用的选项。

15.5. Tapestry

摘自 Tapestry 主页...

Tapestry 是用来创建动态、健壮、高伸缩性 Web 应用的一个 Java 开源框架。 Tapestry 组件构建于标准的Java Servlet API 之上,所以它可以工作在任何 Servlet 容器或者应用服务器之上。

尽管 Spring 有自己的 强有力的 Web 层,但是使用 Tapestry 作为 Web 用户界面,并且结合 Spring 容器管理其他层次,在构建 J2EE 应用上具有一些独到的优势。 这一节将尝试介绍集成这两种框架的最佳实践。

一个使用 Tapestry 和 Spring 构建的 典型的 J2EE 应用通常由 Tapestry 构建一系列的用户界面(UI)层,然后通过一个或多个 Spring容器来连接底层设施。 Tapestry 的 参考手册 包含了这些最佳实践的片断。(下面引用中的 [] 部分是本章的作者所加。)

Tapestry 中一个非常成功的设计模式是保持简单的页面和组件,尽可能多的将任务 委派(delegate) 给 HiveMind [或者 Spring,以及其他容器] 服务。 Listener 方法应该仅仅关心如何组合成正确的信息并且将它传递给一个服务。

那么关键问题就是...如何将协作的服务提供给 Tapestry 页面?答案是,在理想情况下,应该将这些服务直接 注入到 Tapestry 页面中。在 Tapestry 中,你可以使用几种不同的方法 来实现依赖注入。这一节只讨论Spring 提供的依赖注入的方法。Spring-Tapestry 集成真正具有魅力的地方是 Tapestry 优雅又不失灵活的设计,它使得注入 Spring 托管的 bean 简直就像把马鞍搭在马背上一样简单。(另一个好消息是 Spring-Tapestry 集成代码的编写和维护都是由 Tapestry 的创建者 Howard M. Lewis Ship 一手操办, 所以我们应该为了这个如丝般顺畅的集成方案向他致敬。)

15.5.1. 注入 Spring 托管的 beans

假设我们有下面这样一个 Spring 容器定义(使用 XML 格式):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" 
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
 
<beans>
    <!-- the DataSource -->
    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="java:DefaultDS"/>
    </bean>

    <bean id="hibSessionFactory" 
          class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="transactionManager" 
          class="org.springframework.transaction.jta.JtaTransactionManager"/>

    <bean id="mapper" 
          class="com.whatever.dataaccess.mapper.hibernate.MapperImpl">
        <property name="sessionFactory" ref="hibSessionFactory"/>
    </bean>

    <!-- (transactional) AuthenticationService -->
    <bean id="authenticationService" 
          class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="target">
            <bean class="com.whatever.services.service.user.AuthenticationServiceImpl">
                <property name="mapper" ref="mapper"/>
            </bean>
        </property>
        <property name="proxyInterfacesOnly" value="true"/>
        <property name="transactionAttributes">
            <value>
                *=PROPAGATION_REQUIRED
            </value>
        </property>
    </bean>  
 
    <!-- (transactional) UserService -->
    <bean id="userService" 
          class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="target">
             <bean class="com.whatever.services.service.user.UserServiceImpl">
                 <property name="mapper" ref="mapper"/>
             </bean>
        </property>
        <property name="proxyInterfacesOnly" value="true"/>
        <property name="transactionAttributes">
            <value>
                *=PROPAGATION_REQUIRED
            </value>
        </property>
    </bean>  
 
 </beans>

在 Tapestry 应用中,上面的 bean 定义需要 加载到 Spring 容器中, 并且任何相关的 Tapestry 页面都需要提供(被注入) authenticationServiceuserService 这两个 bean, 它们分别实现了 AuthenticationServiceUserService 接口。

现在,Web 应用可以通过调用 Spring 静态工具函数 WebApplicationContextUtils.getApplicationContext(servletContext) 来得到application context。这个函数的参数servletContext 是J2EE Servlet 规范所定义的 ServletContext。 这样一来,页面可以很容易地得到 UserService 的实例, 就像下面的这个例子:

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

这种机制可以工作...如果想进一步改进的话,我们可以将大部分的逻辑封装在页面或组件基类的一个方法中。 然而,这个机制在某些方面违背了 Spring 所倡导的反向控制方法(Inversion of Control)。在理想情况下,页面 不必在context中寻找某个名字的 bean。事实上,页面最好是对context一无所知。

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

[Note]Note

下一节应用于 Tapestry 版本 < 4.0 的情况下。如果你正在使用 Tapestry 4.0+,请参考标有 Section 15.5.1.4, “将 Spring Beans 注入到 Tapestry 页面中 - Tapestry 4.0+ 风格” 的小节。

15.5.1.1. 将 Spring Beans 注入到 Tapestry 页面中

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

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

这个引擎类将 Spring application context作为一个名为 “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>

15.5.1.2. 组件定义文件

现在,我们在页面或组件定义文件(*.page 或者 *.jwc)中添加 property-specification 元素就可以 从 ApplicationContext 中获取 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 表达式使用context中的 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>

15.5.1.3. 添加抽象访问方法

现在在页面或组件本身的 Java 类定义中,我们需要为刚刚定义的属性添加抽象的 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();

下面这个例子总结了前面讲述的方法。这是个完整的 Java 类:

package com.whatever.web.xportal.pages;
 
/**
 *  Allows the user to login, by providing username and password.
 *  After successfully 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 the cookie that identifies a user **/
    private static final String COOKIE_NAME = Login.class.getName() + ".username";  
    private final static int ONE_WEEK = 7 * 24 * 60 * 60;
 
    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();
 
    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 occurred.
        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);
        }

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

15.5.1.4. 将 Spring Beans 注入到 Tapestry 页面中 - Tapestry 4.0+ 风格

在 Tapestry 4.0+ 版本中,将 Spring 托管 beans 注入到 Tapestry 页面是 非常 简单的。 你只需要一个 附加函数库, 和一些(少量)的配置。 你可以将这个库和Web 应用其他的库一起部署。(一般情况下是放在 WEB-INF/lib 目录下。)

你需要使用 前面介绍的方法 来创建Spring 容器。 然后你就可以将 Spring 托管的 beans 非常简单的注入给 Tapestry;如果我们使用 Java5, 我们只需要简单地给 getter 方法添加注释(annotation),就可以将 Spring 管理的 userServiceauthenticationService 对象注入给页面。 比如下面 Login 的例子:(为了保持简洁,许多的类定义在这里省略了)

package com.whatever.web.xportal.pages;

public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {
    
    @InjectObject("spring:userService")
    public abstract UserService getUserService();
    
    @InjectObject("spring:authenticationService")
    public abstract AuthenticationService getAuthenticationService();

}

我们的任务基本上完成了...剩下的工作就是配置HiveMind,将存储在 ServletContext 中 的 Spring 容器配置为一个 HiveMind 服务:

<?xml version="1.0"?>
<module id="com.javaforge.tapestry.spring" version="0.1.1">

    <service-point id="SpringApplicationInitializer"
        interface="org.apache.tapestry.services.ApplicationInitializer"
        visibility="private">
        <invoke-factory>
            <construct class="com.javaforge.tapestry.spring.SpringApplicationInitializer">
                <set-object property="beanFactoryHolder"
                    value="service:hivemind.lib.DefaultSpringBeanFactoryHolder" />
            </construct>
        </invoke-factory>
    </service-point>

    <!-- Hook the Spring setup into the overall application initialization. -->
    <contribution
        configuration-id="tapestry.init.ApplicationInitializers">
        <command id="spring-context"
            object="service:SpringApplicationInitializer" />
    </contribution>

</module>

如果你使用 Java5(这样就可以使用annotation),那么就是这么简单。

如果你不用 Java5,你没法通过annotation来注释你的 Tapestry 页面; 你可以使用传统风格的 XML 来声明依赖注入;例如,在 Login 页面(或组件)的 .page.jwc 文件中:

<inject property="userService" object="spring:userService"/>
<inject property="authenticationService" object="spring:authenticationService"/>

在这个例子中,我们尝试使用声明的方式将定义在 Spring 容器里的 bean 提供给 Tapestry 页面。 页面类并不知道服务实现来自哪里,事实上,你也可以很容易地转换到另一个实现。这在测试中是很有用的。 这样的反向控制是 Spring 框架的主要目标和优点,我们将它拓展到了Tapestry 应用的整个 J2EE 堆栈上。

15.6. WebWork

摘自 WebWork 主页...

WebWork 是一个 Java Web 应用开发框架。这个框架充分考虑了如何提高开发者的效率和简化代码。 它支持构建可重复使用的 UI 模版(例如表单控制),UI 主题,国际化,动态表单参数映射到 JavaBean, 健壮的客户端与服务器端校验等更多功能。

WebWork (在本章作者的眼中)是一个非常简洁、优雅的 Web 框架。它的架构和关键概念容易理解,并且它具有一个丰富的 标签库,漂亮的分离了校验,非常简单高效并且花不了多少时间(另外,它的文档和指南都非常完善)。

WebWork 技术堆栈的一个关键的创新就是提供 一个 IoC 容器 来管理 WebWork Action,处理“绑定(wiring)”的业务对象等等。WebWork 2.2 以前,WebWork 使用自己的 IoC 容器 (并且提供了集成点这样就可以集成其他 IoC 容器例如 Spring 来混合使用)。在WebWork 2.2中,默认使用的 IoC 容器 就是 Spring。对于 Spring 开发者来说这显然是一个好消息,因为它意味着开发人员立即就熟悉了在 WebWork 中的 IoC 配置,习惯用法等等。

根据 DRY (不要重复自己 - Don't Repeat Yourself)的原则,我们没必要再去编写自己的 Spring-WebWork 集成方法了, WebWork 团队已经写了一个。请参考在 WebWork wiki 上的 Spring-WebWork 集成页面

注意 Spring-WebWork 集成代码是由 WebWork 开发者们自己开发的(并且也是由他们负责维护和改进),所以如果 你遇到集成上的问题,第一情况下请参考 WebWork 站点和论坛。虽然这么说,也请大家在 Spring 支持论坛 上自由发表评论和查询 Spring-WebWork 集成方面的问题。

15.7. 更多资源

下面的连接包含了在这一章所提到的 Web 框架的更多资源。

下面的一些 Web 框架的资源可以使你了解更多。