導論 和其他行業一樣,訂做家具行業呈現出這樣一個特點——日益變化的需求應當被反 映到從事該行業的公司使用的軟件中。位於伊利諾斯州的芝加哥RPC Software公司在其產品 中通過使用開源軟件從而在市場中獲得了成功。該公司利用Eclipse RCP、DotProject以及 SugarCRM等技術快速地發布了一個更具有成本效益的解決方案, 從而擊敗了競爭對手。該案 例研究不但揭開了技術層面的面紗,而且總結了開發中獲得知識以及經驗教訓。 業務
RPC Software公司為家具行業開發了ERP訂單管理軟件。在RPC的產品出現前,從該行業的 公司往往使用一些私權軟件(proprietary software),這些軟件基於微軟Visual Studio 程序語言(如Visual Basic)、DOS解決方案和CA的Visual Object。如今的公司都在尋找能 夠處理很多不同業務的解決方案,例如銷售(sale)、報價(quote)、訂單(order entry )、時間追蹤(time tracking)、倉儲(warehousing)、財務管理(accounting)和報表 (reporting)。 因此,能夠滿足這些需求的軟件不僅能夠不斷升級,而且根本上也應該模 塊化。
和其他很多行業一樣,近年來有一個趨勢(drive)——使信息更加透明並且更接近銷售 者和定期與家具經銷商打交道的客戶。這個改變由兩個方面進行驅動。一方面,從事於該產 業的公司紛紛轉向開放數據交換格式,例如OFDA-XML。另一方面,業務流程(如項目跟蹤) 使用Web應用呈報報表,這使得合作公司間共享信息成為可能。
解決方案概述
RPC Software公司的客戶要求軟件能夠快速而明確地適應其業務需求。他們不但要求軟件 擁有強大的客戶端功能以便員工日常使用,而且要求軟件具有為其他不同的層次業務和合作 者的呈報功能。考慮到客戶的這些需求,RPC Software公司決定利用開源軟件作為解決方案 的基礎。RPC Software公司的產品線有一個基於Eclipse RCP和Apache tomat技術的ERP富客 戶端/服務端組件,有一套基於Web的以開源DotProject PHP應用為基礎的項目管理解決方案 ,還有一套即將發布的基於Web的以開源SugarCRM為基礎的CRM產品。
對於項目管理和CRM產品,之所以選擇基於web的解決方案,是因為不必安裝胖客戶端,就可 以在經銷商、客戶和銷售者間共享信息。對於ERP產品,之所以選擇Eclipse RCP是因為 SWT/JFace部件集提供了豐富的功能並且有OSGI提供了模塊化基礎。
Eclipse RCP是 基於OSGi規范構造的,這是其核心所在。維基百科給出的OSGi框架定義如下:
該框 架能實現一個完整的、動態的組件模型。而單獨的Java VM環境正好缺少這個模式。應用程序 (稱為bundle)無需重新引導可以被遠程安裝、啟動、升級和卸載(其中Java包/類的管理被 詳細定義)API中還定義了運行遠程下載管理政策的生命周期管理。服務注冊允許bundles去 檢測新服務和取消的服務,然後相應配合。(http://zh.wikipedia.org/wiki/OSGi)(譯者 注:節選自http://zh.wikipedia.org/wiki/OSGi)
該CORE產品由客戶端和服務器端組成 。從功能組件中的代碼組織到客戶層和服務器層代碼重用,都廣泛地使用了OSGi。Eclipse RCP允許將Java類和資源文件模塊化於jar文件中,這些jar文件為被稱作插件(plugin)。插件 是OSGI包(OSGi bundle)的擴展集。RCP Software客戶端按功能分為不同的核心業務插件。 RCP Software客戶端同樣使用了包含第三方API的插件,例如Hibernate和Jasper Reports。 CORE Business服務器端也是由一組插件構成。這就使在客戶端和服務器端很容易地重用業務 邏輯插件成為可能。
為了進一步簡化應用的部署,RPC應用已經將客戶端和服務器端 打包在同一個安裝包中。在一個插件中,Eclipse RCP通過結合XML和配置文件,定義了入口 點(entry point)概念。在眾多插件中,框架利用依賴元數據以確定哪些插件需要從指定切 入點加以啟動。對於客戶端,基於常規的exe文件的Eclipse RCP應用通過客戶端切入點來完 成啟動過程。客戶端啟動時就會排除那些服務器端功能插件。相似地,當Eclipse RCP作為服 務器端而運行,JNIWrapper會建立Windows服務,此時它利用的是另一個入口點來啟動 Eclipse RCP安裝。安裝中包含了Tomcat服務實例包,而UI邏輯插件或者客戶端相關的插件( 如:SWT插件)都不會被安裝。
Core Business客戶端處理與通常的ERP相關的作業,例如設立提案、裝載票據材料和財務 管理。服務器組件提供了基於web的數據報表的服務,這使公司機構成員間了解更高層次的匯 總報告成為可能。當訂單通過Core Business客戶端提交,在Core Vision產品數據庫中會自 動創建一個產品id。Core Vision中的變化也將呈現在Core Business中。同樣地,在Core Business中CRM的變化也會反映到CORE CRM中,反之亦然。RPC整合了兩個數據庫,而單獨使 用DotProject和SugarCRM的公司則沒法進行這樣的整合。
Eclipse RCP 客戶端
Core Business產品以Eclipse RCP富客戶端框架應用為主。客戶日常使用CORE Business 客戶端以滿足ERP的要求,例如財務管理(accounting)、報表管理(management reporting )、工程造價(project pricing)、設立提案(proposal)等等。與其他技術相比,選擇 Eclipse RCP有很多理由。因為信息輸入和客戶數據量的要求,基於web的應用並不是可行的 選擇。除了Eclipse RCP和SWT,備選的富客戶端部件框架還有C#和Swing,但是本地化的外觀 和感官是SWT的關鍵賣點。對RPC Software公司來說,封裝於 Eclipse RCP內的功能(諸如窗 體、菜單和首選項)也是相當誘人之處。
OSGI和Eclipse RCP提供的模塊化已經被RPC Software公司廣泛地應用於CORE Business客 戶端。客戶通常需要例如定制報表和計算邏輯等功能,但並不是每一個客戶端都需要像Time Entry這樣的功能。基於插件的Eclipse RCP架構,RPC軟件公司分發一系列的核心應用插件和 為客戶特別定制的插件,這使其滿足以上需求成為可能。
Eclipse RCP的插件使用xml文件來告知核心應用該插件有哪些用途。定制報表就是 RPC使用該功能的例子。下面的XML片段展示了在運行時,如何通過添加客戶定制插件 custom.plugin.*.core來添加定制定購報表。
<extension id="xsltTransforms" point="com.rpc.core.xsltTransforms">
<xsltTransform id="com.rpc.core.vendor.model.PurchaseOrder.pdf">
<run class="com.rpc.core.reporting.DefaultTransformSourceProvider">
<parameter name="location" value="reports/PurchaseOrder.xsl" />
</run>
</xsltTransform>
</extension>
該控制樣式的優點在於, 在應用運行時功能所需的配置和元信息都包括在定制插件中。而核心插件或者菜單系統沒有 必要知道新功能的存在。Eclipse RCP框架在運行時會就會發現和應用上述改變。
Eclipse服務器端
RPC Software公司不僅在CORE Business客戶端使用了Eclipse RPC基於插件的架構,在基於Tomcat的CORE Business服務器端亦然。CORE Business服務器通 過基於CORE Business Eclipse RCP客戶端來顯示已輸入的數據報表。設計CORE Business Server過程中,對於重用相同邏輯和客戶端已有的諸如Hibernate和Jasper Reports組件的需 求越發明顯。顯而易見的需求重用的解決方案是將服務器端的Java類重新打包為jar文件,並 將此jar文件包含在WAR文件中。在這個解決方案中,隨著Java類結構的改變,原本復雜的構 建腳本也需要不斷的修正。實際采用的解決方案非常簡單,就是讓Tomcat變地對“插件 敏感”。
Eclipse IDE幫助系統使用了Tomcat的內嵌版本, 這為RPC Software公司設計其需求功能 提供了起點。基於這點,Servlet.jar文件被移到他們自己開發的插件裡。這樣,其他被創建 的依賴servlet API的插件就可以使用它。已有的Tomcat插件在修改後使用Eclipse JDT編譯 器,並非標准的Java編譯器。因此CORE Business只需捆綁JRE而不是JDK。最後,用來加載包 含JSP頁面的插件的類裝載器,改進後被用來加載必要的系統插件,例如 org.eclipse.core.runtime。
package org.eclipse.help.internal.appserver;
public class PluginClassLoaderWrapper extends URLClassLoader {
...
/**
* This is a workaround for the jsp compiler that needs to know the
* classpath.
*/
public URL[] getURLs() {
Set urls = getPluginClasspath(_plugin);
return (URL[]) urls.toArray(new URL[urls.size()]);
}
private Set getPluginClasspath(String pluginId) {
// Collect set of plug-ins
Set plugins = new HashSet();
addPluginWithPrereqs(pluginId, plugins);
// Collect URLs for each plug-in
Set urls = new HashSet();
for (Iterator it = plugins.iterator(); it.hasNext();) {
String id = (String) it.next();
try {
Bundle b = Platform.getBundle(id);
if (b != null) {
// declared classpath
String headers = (String) b.getHeaders().get (Constants.BUNDLE_CLASSPATH);
ManifestElement[] paths =ManifestElement.parseHeader (Constants.BUNDLE_CLASSPATH, headers);
if (paths != null) {
for (int i = 0; i < paths.length; i++) {
String path = paths[i].getValue();
addBundlePath(urls, b, path);
}
} else {
// RPC custom code:
try { String bundleJarPath = b.getLocation();
if (bundleJarPath.equals(Constants.SYSTEM_BUNDLE_LOCATION)) {
SystemBundle systemBundle = (SystemBundle) b;
bundleJarPath = ((SystemBundleData) systemBundle.getBundleData()).getBundleFile().getBaseFile().toURL().getPath ();
bundleJarPath =bundleJarPath.substring (bundleJarPath.lastIndexOf("plugins/"));
} else if(bundleJarPath.startsWith("initial@reference:file:")) {
bundleJarPath =b.getLocation().replaceFirst ("initial@reference:file:", "");
} else {
bundleJarPath =b.getLocation().replaceFirst("update@", "");
}
if (bundleJarPath.endsWith("/")) {
bundleJarPath = bundleJarPath.substring(0, bundleJarPath.lastIndexOf("/"));
}
if (bundleJarPath.startsWith("plugins/") && bundleJarPath.endsWith(".jar")) {
URL installURL = Platform.getInstallLocation().getURL ();
bundleJarPath = installURL.getPath() + bundleJarPath;
urls.add(new URL(installURL.getProtocol(), installURL.getHost(), bundleJarPath));
}
} catch (Exception ex) {
}
// RPC custom code:
}
// dev classpath
String[] devpaths =DevClassPathHelper.getDevClassPath(pluginId);
if (devpaths != null) {
for (int i = 0; i < devpaths.length; i++) {
addBundlePath(urls, b, devpaths[i]);
}
}
}
} catch (BundleException e) {
}
}
return urls;
}
// RPC custom code:
private void addBundlePath(Set urls, Bundle b, String path) {
URL url = b.getEntry(path);
if (url != null) {
try {
urls.add(FileLocator.toFileURL(url));
} catch (IOException ioe) {
}
}
}
// RPC custom code:
...
}
該解決方案較之傳統的Java WAR安裝部署有很多優點。首先,RPC Software公司現在能夠 在客戶端和服務器端使用相同的插件來提升重用和減少維護。其次,安裝部署本質上也不復 雜。對於WAR文件,RPC必須在每個客戶站點上安裝Tomcat,並且為WAR部署。通過將服務器捆 綁成一組即用的Eclipse RCP插件,在獲得可靠性的同時,能在每個客戶端安裝時節省大量的 配置和測試。
浏覽器整合
CORE Business應用中報表兼容的發展過程也非常有意思。隨著應用增多,為滿足客戶需 求很多報表工具都被選擇使用,例如Apache FO、Jasper Reports和Standard Java Printing API。一些報表是基於服務器端,而另一些報表在客戶端直接運行。客戶希望在不切換浏覽器 的前提下,能夠浏覽基於服務器端的報表。RPC能夠通過使用嵌入浏覽器組件的SWT來滿足該 需求。只需一些類似於下面的代碼,就能在基於Eclipse RPC的客戶端上直接獲得基於HTML的 報表,而這些報表都來自於服務器端。
final Browser browser = new Browser(shell, SWT.NONE);
browser.setUrl("http://eclipse.org");
除了顯示從服務器端獲得的報表,客戶也能輕松地使用例如下拉組合框來在客戶端直接選 擇報表的視圖類型。隨後,一個動態的URL生成,並且發送請求至服務器端。
回顧
總而言之,RPC Software公司已經發現Eclipse RCP不但可以滿足其開發需求,而且是個 非常健壯的框架。它可以在維護單一代碼的同時,滿足客戶的不同需求。開源代碼從根本上 已經能夠讓其按需求增加所缺少的功能。基本上,JAVA能夠通過使用眾多API諸如Hibernate 、Apache FO和Jasper Reports進行快速地開發。
RPC Software公司非常滿意Eclipse 作為Eclipse RCP開發的一個IDE。總之,PDE開發環 境能夠讓RCP開發像JAVA開發一樣優秀。眾多可做為插件使用的定制編輯器,使得手動編輯配 置文件更加方便。他們已經能夠利用Eclipse IDE插件(例如Jasper Report Generator插件 和Eclipse TPTP 插件)的優勢來加快開發。
前景展望
在未來,RPC Software公司計劃繼續利用開源軟件來增強其產品。如前所述,未來的CORE CRM產品通過利用SugarCRM提供基於web的一系列解決方案。他們同樣也嘗試著將用於CORE Business的眾多報表技術移植到Eclipse BIRT產品中。最終,通過Eclipse Update站點,將 產品更新和廠商目錄分發到CORE Business客戶端加以安裝,這些將會下個版本的產品中實現 。