15.5. Tapestry

From Spring Framework

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.hibernate3.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 页面都需要提供(被注入) authenticationService 和 userService 这两个 bean, 它们分别实现了 AuthenticationService 和 UserService 接口。

现在,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 所倡导的IoC原则。在理想情况下,页面 不必在context中寻找某个名字的 bean。事实上,页面最好是对context一无所知。

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

注意

下一节应用于 Tapestry 版本 < 4.0 的情况下。如果你正在使用 Tapestry 4.0+,请参考标有 第 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;如果我们使用 Java 5, 我们只需要简单地给 getter 方法添加注释(annotation),就可以将 Spring 管理的 userService 和 authenticationService 对象注入给页面。 比如下面 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>

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

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

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

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