导论
和其他行业一样,订做家具行业呈现出这样一个特点——日益变化的需求应当被反映到从事该行业的公司使用的软件中。位于伊利诺斯州的芝加哥 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 来添加定制定购报表。
<span color="#000066"><extension id=</span><span color="#006600">"xsltTransforms"</span> <span color="#000066">point=</span><span color="#006600">"com.rpc.core.xsltTransforms"</span>><br></br><span color="#000066"><xsltTransform id=</span><span color="#006600">"com.rpc.core.vendor.model.PurchaseOrder.pdf"</span>><br></br><span color="#000066"><run class=</span><span color="#006600">"com.rpc.core.reporting.DefaultTransformSourceProvider"</span>><br></br><span color="#000066"><parameter name=</span><span color="#006600">"location"</span> <span color="#000066">value=</span><span color="#006600">"reports/PurchaseOrder.xsl"</span> /><br></br><span color="#000066"></run><br></br> </xsltTransform><br></br> </extension></span>
该控制样式的优点在于,在应用运行时功能所需的配置和元信息都包括在定制插件中。而核心插件或者菜单系统没有必要知道新功能的存在。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。
<span color="#660033"><strong>package</strong></span> org.eclipse.help.internal.appserver;<br></br><span color="#660033"><strong>public class</strong></span> PluginClassLoaderWrapper <span color="#660033"><strong>extends</strong></span> URLClassLoader {<br></br> ...<p><span color="#006699">/**<br></br> * This is a workaround for the jsp compiler that needs to know the<br></br> * classpath.<br></br> */</span><span color="#660033"><strong>public</strong></span> URL[] getURLs() {</p><br></br> Set urls = getPluginClasspath(<span color="#0000ff">_plugin</span>);<br></br><span color="#660033"><strong>return</strong></span> (URL[]) urls.toArray(<span color="#660033"><strong>new</strong></span> URL[urls.size()]);<br></br> }<p><span color="#660033"><strong>private</strong></span> Set getPluginClasspath(String pluginId) {</p><p><span color="#339966">// Collect set of plug-ins</span> Set plugins = <span color="#660033"><strong>new</strong></span> HashSet();</p><br></br> addPluginWithPrereqs(pluginId, plugins);<p><span color="#339966">// Collect URLs for each plug-in</span> Set urls = <span color="#660033"><strong>new</strong></span> HashSet();</p><br></br><span color="#660033"><strong>for</strong></span> (Iterator it = plugins.iterator(); it.hasNext();) {<br></br> String id = (String) it.next();<br></br><span color="#660033"><strong>try</strong></span> {<br></br> Bundle b = Platform.getBundle(id);<br></br><span color="#660033"><strong>if</strong></span> (b != <span color="#660033"><strong>null</strong></span>) {<p><span color="#339966">// declared classpath</span> String headers = (String) b.getHeaders().get(Constants.<span color="#0000ff">BUNDLE_CLASSPATH</span>);</p><br></br> ManifestElement[] paths =ManifestElement.parseHeader(Constants.<span color="#0000ff">BUNDLE_CLASSPATH</span>, headers);<br></br><span color="#660033"><strong>if</strong></span> (paths != null) {<br></br><span color="#660033"><strong>for</strong></span> (<span color="#660033"><strong>int</strong></span> i = 0; i < paths.<span color="#0000ff">length</span>; i++) {<br></br> String path = paths[i].getValue();<br></br> addBundlePath(urls, b, path);<br></br> }<br></br> } <span color="#660033"><strong>else</strong></span> {<p><span color="#339966"> // RPC custom code:</span><span color="#660033"><strong>try</strong></span> { String bundleJarPath = b.getLocation();</p><br></br> if (bundleJarPath.equals(Constants.<span color="#0000ff">SYSTEM_BUNDLE_LOCATION</span>)) {<br></br> SystemBundle systemBundle = (SystemBundle) b;<br></br> bundleJarPath = ((SystemBundleData) systemBundle.getBundleData()).getBundleFile().getBaseFile().toURL().getPath(); <br></br> bundleJarPath =bundleJarPath.substring(bundleJarPath.lastIndexOf(<span color="#0000ff">"plugins/"</span>));<br></br> } <span color="#660033"><strong>else if</strong></span>(bundleJarPath.startsWith(<span color="#0000ff">"initial@reference:file:"</span>)) {<br></br> bundleJarPath =b.getLocation().replaceFirst(<span color="#0000ff">"initial@reference:file:", ""</span>);<br></br> } <span color="#660033"><strong>else</strong></span> {<br></br> bundleJarPath =b.getLocation().replaceFirst(<span color="#0000ff">"update@", ""</span>);<br></br> }<br></br><span color="#660033"><strong>if</strong></span> (bundleJarPath.endsWith(<span color="#0000ff">"/"</span>)) {<br></br> bundleJarPath = bundleJarPath.substring(0, bundleJarPath.lastIndexOf(<span color="#0000ff">"/"</span>));<br></br> }<br></br><span color="#660033"><strong>if</strong></span> (bundleJarPath.startsWith(<span color="#0000ff">"plugins/"</span>) && bundleJarPath.endsWith(<span color="#0000ff">".jar"</span>)) {<br></br> URL installURL = Platform.getInstallLocation().getURL();<br></br> bundleJarPath = installURL.getPath() + bundleJarPath;<br></br> urls.add(new URL(installURL.getProtocol(), installURL.getHost(), bundleJarPath));<br></br> }<br></br> } <span color="#660033"><strong>catch</strong></span> (Exception ex) {<br></br> }<p><span color="#339966">// RPC custom code:</span> }</p><p><span color="#339966">// dev classpath</span> String[] devpaths =DevClassPathHelper.getDevClassPath(pluginId);</p><br></br><span color="#660033"><strong>if</strong></span> (devpaths != <span color="#660033"><strong>null</strong></span>) {<br></br><span color="#660033"><strong>for (int</strong></span> i = 0; i < devpaths.<span color="#0000ff">length;</span> i++) {<br></br> addBundlePath(urls, b, devpaths[i]);<br></br> }<br></br> }<br></br> }<br></br> } <span color="#660033"><strong>catch</strong></span> (BundleException e) {<br></br> }<br></br> }<br></br><span color="#660033"><strong>return</strong></span> urls;<br></br> }<p><span color="#339966">// RPC custom code:</span><span color="#660033"><strong>private void</strong></span> addBundlePath(Set urls, Bundle b, String path) {</p><br></br> URL url = b.getEntry(path);<br></br><span color="#660033"><strong>if</strong></span> (url != <span color="#660033"><strong>null</strong></span>) {<br></br><span color="#660033"><strong>try</strong></span> {<br></br> urls.add(FileLocator.toFileURL(url));<br></br> } <span color="#660033"><strong>catch</strong></span> (IOException ioe) {<br></br> }<br></br> }<br></br> }<br></br> // RPC custom code:<br></br> ...<br></br> }
该解决方案较之传统的 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);<br></br>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 客户端加以安装,这些将会下个版本的产品中实现。
评论