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 格式):
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
3 "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
4
5<beans>
6 <!-- the DataSource -->
7 <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
8 <property name="jndiName" value="java:DefaultDS"/>
9 </bean>
10
11 <bean id="hibSessionFactory"
12 class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
13 <property name="dataSource" ref="dataSource"/>
14 </bean>
15
16 <bean id="transactionManager"
17 class="org.springframework.transaction.jta.JtaTransactionManager"/>
18
19 <bean id="mapper"
20 class="com.whatever.dataaccess.mapper.hibernate.MapperImpl">
21 <property name="sessionFactory" ref="hibSessionFactory"/>
22 </bean>
23
24 <!-- (transactional) AuthenticationService -->
25 <bean id="authenticationService"
26 class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
27 <property name="transactionManager" ref="transactionManager"/>
28 <property name="target">
29 <bean class="com.whatever.services.service.user.AuthenticationServiceImpl">
30 <property name="mapper" ref="mapper"/>
31 </bean>
32 </property>
33 <property name="proxyInterfacesOnly" value="true"/>
34 <property name="transactionAttributes">
35 <value>
36 *=PROPAGATION_REQUIRED
37 </value>
38 </property>
39 </bean>
40
41 <!-- (transactional) UserService -->
42 <bean id="userService"
43 class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
44 <property name="transactionManager" ref="transactionManager"/>
45 <property name="target">
46 <bean class="com.whatever.services.service.user.UserServiceImpl">
47 <property name="mapper" ref="mapper"/>
48 </bean>
49 </property>
50 <property name="proxyInterfacesOnly" value="true"/>
51 <property name="transactionAttributes">
52 <value>
53 *=PROPAGATION_REQUIRED
54 </value>
55 </property>
56 </bean>
57
58 </beans>
59
60
在 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 所倡导的反向控制方法(Inversion of Control)。在理想情况下,页面 不必在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:
1 package com.whatever.web.xportal;
2
3 import
4
5 public class MyEngine extends org.apache.tapestry.engine.BaseEngine {
6
7 public static final String APPLICATION_CONTEXT_KEY = "appContext";
8
9 /**
10 * @see org.apache.tapestry.engine.AbstractEngine#setupForRequest(org.apache.tapestry.request.RequestContext)
11 */
12 protected void setupForRequest(RequestContext context) {
13 super.setupForRequest(context);
14
15 // insert ApplicationContext in global, if not there
16 Map global = (Map) getGlobal();
17 ApplicationContext ac = (ApplicationContext) global.get(APPLICATION_CONTEXT_KEY);
18 if (ac == null) {
19 ac = WebApplicationContextUtils.getWebApplicationContext(
20 context.getServlet().getServletContext()
21 );
22 global.put(APPLICATION_CONTEXT_KEY, ac);
23 }
24 }
25 }
26
27
这个引擎类将 Spring application context作为一个名为 “appContext” 的属性存放在 Tapestry 应用的 “Global” 对象中。在 Tapestry 应用定义文件中必须保证这个特殊的 IEngine 实例在这个 Tapestry 应用中被使用。 举个例子:
1 file: xportal.application:
2 <?xml version="1.0" encoding="UTF-8"?>
3 <!DOCTYPE application PUBLIC
4 "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
5 "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
6 <application
7 name="Whatever xPortal"
8 engine-class="com.whatever.web.xportal.MyEngine">
9 </application>
10
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 来指定属性的初始值。 整个页面定义文件如下:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE page-specification PUBLIC
3 "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
4 "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
5
6 <page-specification class="com.whatever.web.xportal.pages.Login">
7
8 <property-specification name="username" type="java.lang.String"/>
9 <property-specification name="password" type="java.lang.String"/>
10 <property-specification name="error" type="java.lang.String"/>
11 <property-specification name="callback" type="org.apache.tapestry.callback.ICallback" persistent="yes"/>
12 <property-specification name="userService"
13 type="com.whatever.services.service.user.UserService">
14 global.appContext.getBean("userService")
15 </property-specification>
16 <property-specification name="authenticationService"
17 type="com.whatever.services.service.user.AuthenticationService">
18 global.appContext.getBean("authenticationService")
19 </property-specification>
20
21 <bean name="delegate" class="com.whatever.web.xportal.PortalValidationDelegate"/>
22
23 <bean name="validator" class="org.apache.tapestry.valid.StringValidator" lifecycle="page">
24 <set-property name="required" expression="true"/>
25 <set-property name="clientScriptingEnabled" expression="true"/>
26 </bean>
27
28 <component id="inputUsername" type="ValidField">
29 <static-binding name="displayName" value="Username"/>
30 <binding name="value" expression="username"/>
31 <binding name="validator" expression="beans.validator"/>
32 </component>
33
34 <component id="inputPassword" type="ValidField">
35 <binding name="value" expression="password"/>
36 <binding name="validator" expression="beans.validator"/>
37 <static-binding name="displayName" value="Password"/>
38 <binding name="hidden" expression="true"/>
39 </component>
40
41 </page-specification>
42
15.5.1.3. 添加抽象访问方法
现在在页面或组件本身的 Java 类定义中,我们需要为刚刚定义的属性添加抽象的 getter 方法。 (这样才可以访问那些属性)。
下面这个例子总结了前面讲述的方法。这是个完整的 Java 类:
1
2 package com.whatever.web.xportal.pages;
3
4 /**
5 * Allows the user to login, by providing username and password.
6 * After successfully logging in, a cookie is placed on the client browser
7 * that provides the default username for future logins (the cookie
8 * persists for a week).
9 */
10 public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {
11
12 /** the key under which the authenticated user object is stored in the visit as */
13 public static final String USER_KEY = "user";
14
15 /** The name of the cookie that identifies a user **/
16 private static final String COOKIE_NAME = Login.class.getName() + ".username";
17 private final static int ONE_WEEK = 7 * 24 * 60 * 60;
18
19 public abstract String getUsername();
20 public abstract void setUsername(String username);
21
22 public abstract String getPassword();
23 public abstract void setPassword(String password);
24
25 public abstract ICallback getCallback();
26 public abstract void setCallback(ICallback value);
27
28 public abstract UserService getUserService();
29 public abstract AuthenticationService getAuthenticationService();
30
31 protected IValidationDelegate getValidationDelegate() {
32 return (IValidationDelegate) getBeans().getBean("delegate");
33 }
34
35 protected void setErrorField(String componentId, String message) {
36 IFormComponent field = (IFormComponent) getComponent(componentId);
37 IValidationDelegate delegate = getValidationDelegate();
38 delegate.setFormComponent(field);
39 delegate.record(new ValidatorException(message));
40 }
41
42 /**
43 * Attempts to login.
44 * <p>
45 * If the user name is not known, or the password is invalid, then an error
46 * message is displayed.
47 **/
48 public void attemptLogin(IRequestCycle cycle) {
49
50 String password = getPassword();
51
52 // Do a little extra work to clear out the password.
53 setPassword(null);
54 IValidationDelegate delegate = getValidationDelegate();
55
56 delegate.setFormComponent((IFormComponent) getComponent("inputPassword"));
57 delegate.recordFieldInputValue(null);
58
59 // An error, from a validation field, may already have occurred.
60 if (delegate.getHasErrors()) {
61 return;
62 }
63
64 try {
65 User user = getAuthenticationService().login(getUsername(), getPassword());
66 loginUser(user, cycle);
67 }
68 catch (FailedLoginException ex) {
69 this.setError("Login failed: " + ex.getMessage());
70 return;
71 }
72 }
73
74 /**
75 * Sets up the {@link User} as the logged in user, creates
76 * a cookie for their username (for subsequent logins),
77 * and redirects to the appropriate page, or
78 * a specified page).
79 **/
80 public void loginUser(User user, IRequestCycle cycle) {
81
82 String username = user.getUsername();
83
84 // Get the visit object; this will likely force the
85 // creation of the visit object and an HttpSession
86 Map visit = (Map) getVisit();
87 visit.put(USER_KEY, user);
88
89 // After logging in, go to the MyLibrary page, unless otherwise specified
90 ICallback callback = getCallback();
91
92 if (callback == null) {
93 cycle.activate("Home");
94 }
95 else {
96 callback.performCallback(cycle);
97 }
98
99 IEngine engine = getEngine();
100 Cookie cookie = new Cookie(COOKIE_NAME, username);
101 cookie.setPath(engine.getServletPath());
102 cookie.setMaxAge(ONE_WEEK);
103
104 // Record the user's username in a cookie
105 cycle.getRequestContext().addCookie(cookie);
106 engine.forgetPage(getPageName());
107 }
108
109 public void pageBeginRender(PageEvent event) {
110 if (getUsername() == null) {
111 setUsername(getRequestCycle().getRequestContext().getCookieValue(COOKIE_NAME));
112 }
113 }
114 }
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 管理的 userService 和 authenticationService 对象注入给页面。 比如下面 Login 的例子:(为了保持简洁,许多的类定义在这里省略了)
1 package com.whatever.web.xportal.pages;
2
3 public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {
4
5 @InjectObject("spring:userService")
6 public abstract UserService getUserService();
7
8 @InjectObject("spring:authenticationService")
9 public abstract AuthenticationService getAuthenticationService();
10
11 }
12
我们的任务基本上完成了...剩下的工作就是配置HiveMind,将存储在 ServletContext 中 的 Spring 容器配置为一个 HiveMind 服务:
1<?xml version="1.0"?>
2<module id="com.javaforge.tapestry.spring" version="0.1.1">
3
4 <service-point id="SpringApplicationInitializer"
5 interface="org.apache.tapestry.services.ApplicationInitializer"
6 visibility="private">
7 <invoke-factory>
8 <construct class="com.javaforge.tapestry.spring.SpringApplicationInitializer">
9 <set-object property="beanFactoryHolder"
10 value="service:hivemind.lib.DefaultSpringBeanFactoryHolder" />
11 </construct>
12 </invoke-factory>
13 </service-point>
14
15 <!-- Hook the Spring setup into the overall application initialization. -->
16 <contribution
17 configuration-id="tapestry.init.ApplicationInitializers">
18 <command id="spring-context"
19 object="service:SpringApplicationInitializer" />
20 </contribution>
21
22</module>
23
24
如果你使用 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 堆栈上。
posted on 2008-10-04 23:24
Blog of JoJo 阅读(672)
评论(0) 编辑 收藏 所属分类:
Programming 相关