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 閱讀(683)
評論(0) 編輯 收藏 所屬分類:
Programming 相關