http://www.ibm.com/developerworks/cn/java/j-lo-jeeflex/?S_TACT=105AGX52&S_CMP=tec-csdn
2009 年 8 月 05 日
傳統的 Java EE 應用程式通常使用某種 MVC 架構(例如,Struts)作為前端使用者介面,隨著 Flex 的興起,基於 RIA 的用戶端能夠給使用者帶來更酷的介面,更短的回應時間,以及更接近於傳統型應用程式的體驗。本文將講述如何將 Flex 整合至一個現有的 Java EE 應用程式中,以及如何應用最佳實務高效率地並行開發 Java EE 和 Flex。
開發環境
本文的開發環境為 Windows 7 Ultimate,Eclipse 3.4,Flex Builder 3(從 參考資源 獲得下載連結)。Java EE 伺服器使用 Resin 3.2,當然,您也可以使用 Tomcat 等其他 Java EE 伺服器。
現有的 Java EE 應用
假定我們已經擁有了一個管理僱員資訊的 Java EE 應用,名為 EmployeeMgmt-Server,結構如 圖 1 所示:
圖 1. Java EE 工程結構
這是一個典型的 Java EE 應用,使用了流行的 Spring 架構。為了簡化資料庫操作,我們使用了記憶體資料庫 HSQLDB。對這個簡單的應用,省略了 DAO,直接在 Façade 中通過 Spring 的 JdbcTemplate 操作資料庫。最後,EmployeeMgmt 應用通過 Servlet 和 JSP 頁面為使用者提供前端介面:
圖 2. EmployeeMgmt Web 介面
該介面為傳統的 HTML 頁面,使用者每次點擊某個連結都需要重新整理頁面。由於 Employee Management 系統更接近於傳統的傳統型應用程式,因此,用 Flex 重新編寫介面會帶來更好的使用者體驗。
整合 BlazeDS
如何將 Flex 整合至該 Java EE 應用呢?現在,我們希望用 Flex 替換掉原有的 Servlet 和 JSP 頁面,就需要讓 Flex 和 Java EE 後端通訊。Flex 支援多種遠程調用方式,包括 HTTP,Web Services 和 AMF。不過,針對 Java EE 開發的伺服器端應用,可以通過整合 BlazeDS,充分利用 AMF 協議並能輕易與 Flex 前端交換資料,這種方式是 Java EE 應用程式整合 Flex 的首選。
BlazeDS 是 Adobe LifeCycle Data Services 的開源版本,遵循 LGPL v3 授權,可以免費使用。BlazeDS 為 Flex 提供了基於 AMF 二進位協議的遠程調用支援,其作用相當於 Java 的 RMI。有了 BlazeDS,通過簡單的配置,一個 Java 介面就可以作為服務暴露給 Flex,供其遠程調用。
儘管現有的 EmployeeMgmt 應用程式已經有了 Façade 介面,但這個介面是暴露給 Servlet 使用的,最好能再為 Flex 定義另一個介面 FlexService,並隱藏 Java 語言的特定對象(如 清單 1 所示):
清單 1. FlexService interface
public interface FlexService { Employee createEmployee(String name, String title, boolean gender, Date birth); void deleteEmployee(String id); Employee[] queryByName(String name); Employee[] queryAll(); } |
現在,Java EE 後端與 Flex 前端的介面已經定義好了,要完成 Java EE 後端的介面實作類別非常容易,利用 Spring 強大的依賴注入功能,可以通過幾行簡單的程式碼完成:
清單 2. FlexServiceImpl class
public class FlexServiceImpl implements FlexService { private static final Employee[] EMPTY_EMPLOYEE_ARRAY = new Employee[0]; private Facade facade; public void setFacade(Facade facade) { this.facade = facade; } public Employee createEmployee(String name, String title, boolean gender, Date birth) { return facade.createEmployee(name, title, gender, birth); } public void deleteEmployee(String id) { facade.deleteEmployee(id); } public Employee[] queryAll() { return facade.queryAll().toArray(EMPTY_EMPLOYEE_ARRAY); } public Employee[] queryByName(String name) { return facade.queryByName(name).toArray(EMPTY_EMPLOYEE_ARRAY); } } |
然後,我們將 BlazeDS 所需的 jar 包放至 /WEB-INF/lib/
。BlazeDS 需要如下的 jar:
清單 3. BlazeDS 依賴的 Jar
backport-util-concurrent.jar commons-httpclient.jar commons-logging.jar flex-messaging-common.jar flex-messaging-core.jar flex-messaging-proxy.jar flex-messaging-remoting.jar |
在 web.xml 中添加 HttpFlexSession 和 Servlet 映射。HttpFlexSession 是 BlazeDS 提供的一個 Listener,負責監聽 Flex 遠程調用請求,並進行一些初始化設定:
清單 4. 定義 Flex Listener
<listener> <listener-class>flex.messaging.HttpFlexSession</listener-class> </listener> |
MessageBrokerServlet 是真正處理 Flex 遠程調用請求的 Servlet,我們需要將其映射到指定的 URL:
清單 5. 定義 Flex servlet
<servlet> <servlet-name>messageBroker</servlet-name> <servlet-class>flex.messaging.MessageBrokerServlet</servlet-class> <init-param> <param-name>services.configuration.file</param-name> <param-value>/WEB-INF/flex/services-config.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>messageBroker</servlet-name> <url-pattern>/messagebroker/*</url-pattern> </servlet-mapping> |
BlazeDS 所需的所有設定檔均放在 /WEB-INF/flex/ 目錄下。BlazeDS 將讀取 services-config.xml 設定檔,該設定檔又引用了 remoting-config.xml、proxy-config.xml 和 messaging-config.xml 這 3 個設定檔,所以,一共需要 4 個設定檔。
由於 BlazeDS 需要將 Java 介面 FlexService 暴露給 Flex 前端,因此,我們在設定檔 remoting-config.xml 中將 FlexService 介面聲明為一個服務:
清單 6. 定義 flexService 服務
<destination id="flexService"> <properties> <source>org.expressme.employee.mgmt.flex.FlexServiceImpl</source> <scope>application</scope> </properties> </destination> |
服務名稱通過 destination 的 id 屬性指定,Flex 前端通過該服務名稱來進行遠程調用。scope 指定為 application,表示該對象是一個全域對象。
然而,按照預設的聲明,BlazeDS 會去執行個體化 FlexService 對象。對於一個 Java EE 應用來說,通常這些服務物件都是被容器管理的(例如,Spring 容器或 EJB 容器),更合適的方法是尋找該服務物件而非直接執行個體化。因此,需要告訴 BlazeDS 通過 Factory 來尋找指定的 FlexService 對象,修改配置如下:
清單 7. 通過 factory 定義 flexService
<destination id="flexService"> <properties> <factory>flexFactory</factory> <source>flexService</source> <scope>application</scope> </properties> </destination> |
現在,Flex 如何才能通過 BlazeDS 調用 FlexService 介面呢?由於 FlexService 對象已經被 Spring 管理,因此,我們需要編寫一個 FlexFactory 告訴 BlazeDS 如何找到 Spring 管理的 FlexService 的執行個體。flexFactory 在 services-config.xml 中指定:
清單 8. 定義 flexFactory
<factories> <factory id="flexFactory" class="org.expressme.employee.mgmt.flex.FlexFactoryImpl"/> </factories> |
FlexFactoryImpl 實現了 FlexFactory 介面,該介面完成兩件事情:
- 建立 FactoryInstance 對象;
- 通過 FactoryInstance 對象尋找我們需要的 FlexService。
因此,需要一個 FactoryInstance 的實作類別,我們編寫一個 SpringFactoryInstance,以便從 Spring 的容器中尋找 FlexService:
清單 9. SpringFactoryInstance class
class SpringFactoryInstance extends FactoryInstance { private Log log = LogFactory.getLog(getClass()); SpringFactoryInstance(FlexFactory factory, String id, ConfigMap properties) { super(factory, id, properties); } public Object lookup() { ApplicationContext appContext = WebApplicationContextUtils. getRequiredWebApplicationContext( FlexContext.getServletConfig().getServletContext() ); String beanName = getSource(); try { log.info("Lookup bean from Spring ApplicationContext: " + beanName); return appContext.getBean(beanName); } catch (NoSuchBeanDefinitionException nex) { ... } catch (BeansException bex) { ... } catch (Exception ex) { ... } } } |
FlexFactoryImpl 負責執行個體化 SpringFactoryInstance 並通過 SpringFactoryInstance 的 lookup()
方法尋找 FlexService 介面對象:
清單 10. FlexFactoryImpl class
public class FlexFactoryImpl implements FlexFactory { private Log log = LogFactory.getLog(getClass()); public FactoryInstance createFactoryInstance(String id, ConfigMap properties) { log.info("Create FactoryInstance."); SpringFactoryInstance instance = new SpringFactoryInstance(this, id, properties); instance.setSource(properties.getPropertyAsString(SOURCE, instance.getId())); return instance; } public Object lookup(FactoryInstance instanceInfo) { log.info("Lookup service object."); return instanceInfo.lookup(); } public void initialize(String id, ConfigMap configMap) { } } |
以下是 BlazeDS 尋找 FlexService 介面的過程:
- BlazeDS 將首先建立 FlexFactory 的執行個體—— FlexFactoryImpl;
- 當接收到 Flex 前端的遠程調用請求時,BlazeDS 通過 FlexFactory 建立 FactoryInstance 對象,並傳入請求的 Service ID。在這個應用程式中,被建立的 FactoryInstance 實際對象是 SpringFactoryInstance;
- FactoryInstance 的 lookup() 方法被調用,在 SpringFactoryInstance 中,首先尋找 Spring 容器,然後,通過 Bean 的 ID 尋找 Bean,最終,FlexService 介面的執行個體被返回。
注意到 destination 的 id 並沒有寫死在代碼中,而是通過以下語句獲得的:
清單 11. 擷取 destination 的 ID
properties.getPropertyAsString(SOURCE, instance.getId()) |
Property 的 SOURCE 屬性由 BlazeDS 讀取 XML 設定檔獲得:
清單 12. 配置 destination 的 id
<destination id="flexService"> <properties> <factory>flexFactory</factory> <source>flexService</source> <scope>application</scope> </properties> </destination> |
如果您沒有使用 Spring 架構,也不要緊,只需修改 FactoryInstance 的 lookup() 方法。例如,對於一個 EJB 來說,lookup() 方法應該通過 JNDI 尋找返回遠程介面。無論應用程式結構如何,我們的最終目標是向 BlazeDS 返回一個 FlexService 的執行個體對象。
開發 Flex 用戶端
首先安裝 Flex Builder 3,可以在 Adobe 的官方網站獲得 30 天免費試用。然後,開啟 Flex Builder 3,建立一個新的 Flex Project,命名為 EmployeeMgmt-Flex:
圖 3. 建立 Flex 工程 - 第一步
Flex Project 需要指定 Server 端的設定檔地址:
圖 4. 建立 Flex 工程 - 第二步
因此,需要填入 EmployeeMgmt-Server 項目的 web 根目錄,該目錄下必須要存在 /WEB-INF/flex/
。點擊“Validate Configuration”驗證設定檔是否正確,只有通過驗證後,才能繼續。預設地,Flex Builder 將會把產生的 Flash 檔案放到 EmployeeMgmt-Server 項目的 web/EmployeeMgmt-Flex-debug
目錄下。
一個 Flex Project 的目錄結構如下:
圖 5. Flex 工程的目錄結構
用 Flex Builder 做出漂亮的使用者介面非常容易。Flex Builder 提供了一個可視化的編輯器,通過簡單的拖拽,一個毫無經驗的開發人員也能夠設計出漂亮的布局。如果熟悉一點 XML 的知識,編輯 MXML 也並非難事。我們設計的 Employee Management 系統介面的最終效果如下:
圖 6. 用 Flex Builder 的可視化編輯器設計介面
本文不打算討論如何編寫 Flex 介面,而是把重點放在如何?遠程調用。
為了能在 Flex 中實現遠程調用,我們需要定義一個 RemoteObject 對象。可以通過 ActionScript 編碼建立該對象,也可以直接在 MXML 中定義一個 RemoteObject 對象,並列出其所有的方法:
清單 13. 定義 flexServiceRO
<mx:RemoteObject id="flexServiceRO" destination="flexService"> <mx:method name="queryAll" result="handleQueryAll(result : ResultEvent)"/> </mx:RemoteObject> |
現在,就可以調用這個名為 flexServiceRO 的 RemoteObject 對象的方法了:
清單 14. 調用 FlexServiceRO.queryAll()
flexServiceRO.queryAll(function(result : ResultEvent) { var employees = result.result as Array; }); |
運行該 Flex Application,僱員資訊已經被正確擷取了:
圖 7. 在瀏覽器中運行 Flex application
增強 RemoteObject 對象
通過 RemoteObject 進行調用雖然簡單,但存在不少問題:首先,RemoteObject 是一個 Dynamic Class,Flex Builder 的編譯器無法替我們檢查參數類型和參數個數,這樣,在編寫 ActionScript 代碼時極易出錯。此外,介面變動時(這種情況常常發生),需要重新修改 RemoteObject 的定義。此外,Flex 團隊需要一份隨時修訂的完整的 FlexService 介面文檔才能工作。
因此,最好能使用強型別的 RemoteObject 介面,讓 Flex Builder 的編譯器及早發現錯誤。這個強型別的 RemoteObject 最好能通過 Java EE 應用的 FlexService 介面自動產生,這樣,就無需再維護 RemoteObject 的定義。
為了能完成自動產生 RemoteObject 對象,我編寫了一個 Java2ActionScript 的 Ant 任務來自動轉換 FlexService 介面以及相關的所有 JavaBean。JavaInterface2RemoteObjectTask 完成一個 Java 介面對象到 RemoteObject 對象的轉換。使用如下的 Ant 指令碼:
清單 15. 產生 ActionScript class 的 Ant 指令碼
<taskdef name="genactionscript" classname="org.expressme.ant.JavaBean2ActionScriptTask"> <classpath refid="build-classpath" /> </taskdef> <taskdef name="genremoteobject" classname="org.expressme.ant.JavaInterface2RemoteObjectTask"> <classpath refid="build-classpath" /> </taskdef> <genactionscript packageName="org.expressme.employee.mgmt" includes="Employee" orderByName="true" encoding="UTF-8" outputDir="${gen.dir}"/> <genremoteobject interfaceClass="org.expressme.employee.mgmt.flex.FlexService" encoding="UTF-8" outputDir="${gen.dir}" destination="flexService"/> |
轉換後的 FlexServiceRO 類擁有 Java 介面對應的所有方法,每個方法均為強型別簽名,並添加額外的兩個可選的函數處理 result 和 fault 事件。例如,queryByName 方法:
清單 16. 自動產生的 queryByName() 方法
public function queryByName(arg1 : String, result : Function = null, fault : Function = null) : void { var op : AbstractOperation = ro.getOperation("queryByName"); if (result!=null) { op.addEventListener(ResultEvent.RESULT, result); } if (fault!=null) { op.addEventListener(FaultEvent.FAULT, fault); } var f : Function = function() : void { op.removeEventListener(ResultEvent.RESULT, f); op.removeEventListener(FaultEvent.FAULT, f); if (result!=null) { op.removeEventListener(ResultEvent.RESULT, result); } if (fault!=null) { op.addEventListener(FaultEvent.FAULT, fault); } } op.addEventListener(ResultEvent.RESULT, f); op.addEventListener(FaultEvent.FAULT, f); op.send(arg1); } |
轉換 Java 介面是通過 Interface.as 和 InterfaceMethod.as 兩個模板檔案完成的,此外,所有在 Java EE 後端和 Flex 之間傳遞的 JavaBean 對象也通過 JavaBean2ActionScriptTask 自動轉換成對應的 ActionScript 類,這是通過 Bean.as 模板完成的。
有了 Java 類到 ActionScript 的自動轉換,我們在編寫 ActionScript 時,就能享受到編譯器檢查和 ActionScript 類方法的自動提示了:
圖 8. Flex Builder 的代碼自動補全
唯一的缺憾是通過反射讀取 FlexService 介面時,我們失去了方法的參數名稱,因此,FlexServiceRO 的方法參數名只能變成 arg1,arg2 …… 等,要讀取 FlexService 介面的方法參數名,只能通過解析 Java 原始碼實現。
現在,Java EE 後端Team Dev和 Flex 前端Team Dev只需協商定義好 FlexService 介面,然後,利用 Java2ActionScript,Flex 團隊就得到了強型別的 FlexServiceRO 類,而 Java EE 團隊則只需集中精力實現 FlexService 介面。
在開發的前期,甚至可以用硬式編碼 FlexService 的實作類別。每當 FlexService 變動時,只需再次運行 Ant 指令碼,就可以獲得最新的 FlexServiceRO 類。這樣,兩個團隊都可以立刻開始工作,僅需要通過 FlexService 介面就可以完美地協同開發。