?
本文檔使用 PHP中文網(wǎng)手冊 發(fā)布
摘自 Tapestry 主頁...
“ Tapestry 是用來創(chuàng)建動態(tài)、健壯、高伸縮性 Web 應用的一個 Java 開源框架。 Tapestry 組件構(gòu)建于標準的Java Servlet API 之上,所以它可以工作在任何 Servlet 容器或者應用服務(wù)器之上。 ”盡管 Spring 有自己的 強有力的 Web 層,但是使用 Tapestry 作為 Web 用戶界面,并且結(jié)合 Spring 容器管理其他層次,在構(gòu)建 J2EE 應用上具有一些獨到的優(yōu)勢。 這一節(jié)將嘗試介紹集成這兩種框架的最佳實踐。
一個使用 Tapestry 和 Spring 構(gòu)建的 典型的 J2EE 應用通常由 Tapestry
構(gòu)建一系列的用戶界面(UI)層,然后通過一個或多個 Spring容器來連接底層設(shè)施。
Tapestry 的 參考手冊
包含了這些最佳實踐的片斷。(下面引用中的 []
部分是本章的作者所加。)
那么關(guān)鍵問題就是...如何將協(xié)作的服務(wù)提供給 Tapestry 頁面?答案是,在理想情況下,應該將這些服務(wù)直接 注入到 Tapestry 頁面中。在 Tapestry 中,你可以使用幾種不同的方法 來實現(xiàn)依賴注入。這一節(jié)只討論Spring 提供的依賴注入的方法。Spring-Tapestry 集成真正具有魅力的地方是 Tapestry 優(yōu)雅又不失靈活的設(shè)計,它使得注入 Spring 托管的 bean 簡直就像把馬鞍搭在馬背上一樣簡單。(另一個好消息是 Spring-Tapestry 集成代碼的編寫和維護都是由 Tapestry 的創(chuàng)建者 Howard M. Lewis Ship 一手操辦, 所以我們應該為了這個如絲般順暢的集成方案向他致敬。)
假設(shè)我們有下面這樣一個 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 容器中,
并且任何相關(guān)的 Tapestry 頁面都需要提供(被注入) authenticationService
和 userService
這兩個 bean,
它們分別實現(xiàn)了 AuthenticationService
和 UserService
接口。
現(xiàn)在,Web 應用可以通過調(diào)用 Spring 靜態(tài)工具函數(shù) WebApplicationContextUtils.getApplicationContext(servletContext)
來得到application context。這個函數(shù)的參數(shù)servletContext 是J2EE Servlet 規(guī)范所定義的 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 已經(jīng)提供了一種給頁面聲明屬性的方法, 事實上,以聲明的方式管理一個頁面上的所有屬性是首選的方法,這樣 Tapestry 能夠?qū)傩缘纳芷? 作為頁面和組件生命周期的一部分加以管理。
下一節(jié)應用于 Tapestry 版本 < 4.0 的情況下。如果你正在使用 Tapestry 4.0+,請參考標有 第?15.5.1.4?節(jié) “將 Spring Beans 注入到 Tapestry 頁面中 - Tapestry 4.0+ 風格” 的小節(jié)。
首先我們需要 Tapestry 頁面組件在沒有 ServletContext 的情況下訪問
ApplicationContext;這是因為在頁面/組件生命周期里面,當我們需要訪問
ApplicationContext 時,ServletContext
并不能被頁面很方便的訪問到,所以我們不能直接使用 WebApplicationContextUtils.getApplicationContext(servletContext)
。
一種解決方法就是實現(xiàn)一個自定義的 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"; 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>
現(xiàn)在,我們在頁面或組件定義文件(*.page 或者 *.jwc)中添加 property-specification 元素就可以
從 ApplicationContext
中獲取 bean,并為這些 bean 創(chuàng)建頁面或
組件屬性。例如:
<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>
現(xiàn)在在頁面或組件本身的 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();
下面這個例子總結(jié)了前面講述的方法。這是個完整的 Java 類:
package com.whatever.web.xportal.pages; public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener { public static final String USER_KEY = "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)); } 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; } } 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)); } } }
在 Tapestry 4.0+ 版本中,將 Spring 托管 beans 注入到 Tapestry 頁面是 非常 簡單的。
只需要一個 附加函數(shù)庫,
和一些(少量)的配置。
可以將這個庫和Web 應用其他的庫一起部署。(一般情況下是放在 WEB-INF/lib
目錄下。)
你需要使用 前面介紹的方法 來創(chuàng)建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(); }
我們的任務(wù)基本上完成了...剩下的工作就是配置HiveMind,將存儲在 ServletContext
中
的 Spring 容器配置為一個 HiveMind 服務(wù):
<?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 頁面;
你可以使用傳統(tǒng)風格的 XML 來聲明依賴注入;例如,在 Login
頁面(或組件)的 .page
或 .jwc
文件中:
<inject property="userService" object="spring:userService"/> <inject property="authenticationService" object="spring:authenticationService"/>
在這個例子中,我們嘗試使用聲明的方式將定義在 Spring 容器里的 bean 提供給 Tapestry 頁面。 頁面類并不知道服務(wù)實現(xiàn)來自哪里,事實上,你也可以很容易地轉(zhuǎn)換到另一個實現(xiàn)。這在測試中是很有用的。 這樣的反向控制是 Spring 框架的主要目標和優(yōu)點,我們將它拓展到了Tapestry 應用的整個 J2EE 堆棧上。