取得领先
VMware 提供培训和认证,以加速您的进步。
了解更多这是本系列中唯一一篇实际使用 Spring 框架的博客。Spring 用于配置 Spring Dynamic Modules 并发布和使用 OSGi 服务。它还演示了一种将 Spring 管理的 bean 与 GWT 远程处理联系起来的机制。但是,我很清楚 Spring/GWT 集成本身就是一个重要主题,所以我特意在此坚持一个简单的解决方案。
请参阅第 1 部分,了解 GWT StockWatcher 示例和我正在使用的软件的背景。
另请注意,您可以跳过所有这些繁琐的说明,直接跳到底部的下载摘要。
在第 2 部分中,我们从 WAR 文件中删除了 GWT 依赖项,并将它们转换为安装到 dm Server 存储库中的 OSGi 包。 完成此操作后,我们就可以部署任意数量使用 GWT 远程处理的应用程序,而无需在 WAR 中包含任何 GWT 依赖项。
在最后一部分中,我们将使用 Spring Dynamic Modules 进一步模块化应用程序。你可能会问的一个问题是……为什么?这不是一个坏问题——没有人愿意为了它而不必要地使他们的代码复杂化。在决定共享服务方法是否有帮助时,您可以将其归结为一些简单的问题
- 我的应用程序中是否有任何部分可能需要被另一个应用程序使用?- 我的应用程序中的不同组件是否会以不同的速率演变?- 我是否可能需要维护同一组件的多个并发版本?- 我是否希望能够在应用程序运行时部署组件的更改?
看看我们的 Stockwatcher 应用程序,它目前仅限于一个股票市场。我们知道世界各地有许多不同的市场,所以为了使其更灵活,赋予它访问不同市场的能力当然是有意义的,所以这就是我们要做的——将我们的股票市场变成共享服务。一个包将包含一个共享 API,该 API 定义了市场可以做什么。其他包可以是该 API 的实现——也许一个是伦敦的,一个是纽约的。然后,我们将能够部署我们想要的市场,启动它,停止它,取消部署它,并用另一个市场替换它——所有这些都在运行时进行。
我知道将单个 WAR 文件转换为共享服务的流程可能比从头开始以模块化方式设计它要普遍得多,因此本博客将进一步构建我们在 第 2 部分 中所做的工作,而不是重新开始。您可以从第 2 部分此处下载 Eclipse 项目,以及第 3 部分中所有工作的完成项目此处(如果您这样做,则需要定义 GWT_ROOT_INSTALL 类路径变量)。希望我在之前的博客中提出的一些关于划分源代码的建议最终会变得有意义!
要在 Eclipse 中创建一个简单的包项目,请右键单击并选择新建->其他->SpringSource dm Server->包项目。这将创建一个框架MANIFEST.MF供您入门。我们不需要将其设为 Spring Dynamic Module,因为它唯一的目的是提供 API,并且它本身不会有任何 bean 实例,所以普通的包就可以了。
- 将项目名称指定为 StockWatcherServiceAPI- 如果您愿意,可以通过单击“项目布局”中的“配置默认值...”链接来配置源目录。为了保持一致性,我将其设置为src/main/java- 将包名称和符号名称设置为com.google.gwt.sample.stockwatcher.client.api并将版本保留为1.0.0- 模块类型应为无,目标运行时应为您的 dm Server 实例。如果您没有选择此选项,则需要单击“新建”并创建一个。- 现在您可以单击“完成”,您应该会看到您的新包项目以及生成的MANIFEST.MF.
接下来要做的是将 API 代码移动到新项目中。将整个com.google.gwt.sample.stockwatcher.client.api包从 StockWatcherWar 项目拖放到新的包项目中。这是客户端和服务器共享的所有代码。它应该包含StockPriceService接口以及StockPrice和DelistedException类。如果成功,几乎所有内容都应该被破坏!
接下来,让我们考虑一下我们需要从新包中导出什么。在本例中,这很简单——来自一个包的 API。在MANIFEST.MF编辑器的“运行时”选项卡中,将包添加到“导出的包”列表并保存。
现在我们需要考虑我们的 API 代码需要哪些依赖项,换句话说,为什么它不再构建了。显然,我们在第 2 部分中创建的 GWT 包将是一个良好的开端。在“依赖项”选项卡中,将 com.google.gwt 包添加到“导入包”列表(如果未列出,则可能不在 dm Server 存储库中,这在 第 2 部分 中有解释)并保存。这应该可以解决新项目中的所有构建问题,但其他项目应该仍然损坏。现在,让我们忽略这一点,因为还有更多重构工作要做。
此时,StockWatcherServiceAPI 项目应如下所示
及其MANIFEST.MF应如下所示
使用步骤 1 中的说明创建一个名为“StockWatcherServiceLondon”的新包项目,并为其指定包名称 com.google.gwt.sample.stockwatcher.service.london
因此,让我们考虑一下这个包需要什么。它需要代码来实现伦敦股票价格服务,以及将该服务作为共享服务导出到 OSGi 注册表的方法。第一部分是一个简单的重构工作,因为我们已经有了一些可以重用的服务代码。对于第二部分,将包转换为 Spring Dynamic Module 将是最简单的方法,因为它使使用 Spring bean 发布和使用 OSGi 服务变得非常简单。
首先要做的是。返回并查看StockPriceServiceImpl.java中的服务实现。您将看到StockPriceService代码与RemoteServiceServlet绑定。servlet 需要保留才能使远程处理正常工作,但我们需要提取实现代码并使用它来创建共享服务。稍后在第 4 部分中,servlet 需要进行调整以使用我们创建的服务。
因此,复制com.google.gwt.sample.stockwatcher.server包从 StockWatcherWar 项目并将其粘贴到 StockWatcherServiceLondon 的src/main/java中。您现在应该有一个StockPriceServiceImpl.java的副本,可以对其进行重构。
删除extends RemoteServiceServlet、RemoteServiceServletimport 和SerialVersionUID字段,因为现在不再需要这些字段。保存更改后,您将看到我们需要为包导入一些依赖项——特别是步骤 1 中创建的 API 包。
编辑MANIFEST.MF以添加com.google.gwt.sample.stockwatcher.client.api的导入包。保存时,您应该会在清单中看到一条错误消息,指出无法解析该包。我们做错了什么?!问题是,当您在 Eclipse 中的包项目之间进行导入和导出时,需要告诉工具允许项目共享引用。右键单击 StockWatcherServiceLondon 并选择“属性”->“项目引用”。勾选 StockWatcherServiceAPI 的复选框,然后单击“确定”。然后,您需要对MANIFEST.MF进行假编辑以获取更改。您现在应该会看到 StockWatcherServiceAPI 列为包依赖项。让我明确一点,一旦从 Eclipse 导出了一个包,您就不需要执行此额外步骤——只有当您想要在包只是项目时在它们之间创建依赖项时才需要执行此步骤。
您应该还有一个问题需要解决。尽管 com..client.api 包导入了 com.google.gwt 包,但导入对于该包是私有的。导入 com..client.api 包的任何包都不会继承此依赖项——必须明确进行。因此,在MANIFEST.MF中,添加Import-Bundle用于 com.google.gwt 包并保存。如果您仍然在StockPriceServiceImpl.java中看到问题,则可能需要对其进行假编辑,否则您做错了什么。
太棒了!我们现在已经完成了一半创建共享服务的工作。这是它目前的样子
下一步是将 StockPriceServiceLondon 转换为 OSGi 服务。我们将使用 Spring Dynamic Modules 来做到这一点。Spring Dynamic Module 的原理很简单——您在/META-INF/spring
中提供 Spring 配置文件 当 bundle 部署时,会为您创建一个 /META-INF/spring 文件夹和一个 ApplicationContext。这是一种将 Spring 管理的 bean 导出为 OSGi 服务的优秀且非常简单的方法。所以,创建一个/spring文件夹,位于/META-INF下(右键单击/META-INF并选择新建->文件夹)。然后右键单击新建的/spring文件夹,选择新建->Spring Bean 定义。您可以随意命名,但在我的示例中,它被称为serviceLondon-config.xml。输入名称并单击完成。现在我们需要将StockPriceServiceImpl定义为 Spring bean,以便在部署 bundle 时创建它的实例。如果您熟悉 Spring,这应该很容易。如果不熟悉,则需要插入以下 XML
<bean id="stockPrices" class="com.google.gwt.sample.stockwatcher.server.StockPriceServiceImpl"/>
最后,我们需要将此 bean 导出为 OSGi 服务。我们可以在同一个配置文件中执行此操作,但最好将 OSGi 依赖项分开。因此,创建另一个名为 osgi-config.xml 的 Spring Bean 定义,这次单击“下一步”,然后选中 osgi 命名空间复选框,再单击“完成”。要将 bean 导出为 OSGi 服务,请插入以下 XML
<osgi:service interface="com.google.gwt.sample.stockwatcher.client.api.StockPriceService" ref="stockPrices"/>
就是这样!我们现在有一个创建共享服务的 bundle。在继续之前,最好对其进行测试,以确保其行为符合预期。
我们已经构建了一个 Spring Dynamic Module,因此对于这个新的 TestConsumer 模块,我只会突出显示差异。该模块需要导入 com.google.gwt.sample.stockwatcher.client.api bundle(或包),并且需要一些 Java 代码来调用服务客户端。您需要在/META-INF/spring
中提供 Spring 配置文件 文件夹中有两个 spring 配置文件一个用于测试 bean(例如)
<bean id="consumer" class="com.ben.consumer.Consumer"> <constructor-arg ref="priceService"/> </bean>
testConsumer-config.xml),另一个用于<osgi:reference以使用该服务(例如)
<osgi:reference id="priceService" interface="com.google.gwt.sample.stockwatcher.client.api.StockPriceService"/>
osgi-config.xml)。在 Java 代码中,只需使用一些任意字符串调用service.getPrices(new String{"foo", "bar"})StockPrice,您应该会得到一个对象数组。如果要将输出记录到System.err,它会出现在 dm Server 的跟踪目录中.
<dm server 安装目录>/serviceability/trace/<测试 bundle 的名称>/trace.log要在 STS 中运行快速而粗糙的测试,您需要启动 dm Server。然后,将整个 StockWatcherServiceAPI 项目拖放到服务器上。当项目成功部署后,您应该会收到如下控制台消息“com.google.gwt.sample.stockwatcher.client.api”版本“1”的部署已完成
。接下来,将整个 StockPriceServiceLondon 项目拖放到服务器上,并等待部署消息。如果全部成功初始化,请将您的测试项目拖放到服务器上。如果希望服务器每次都干净启动(清除旧的跟踪输出等),请在启动配置中添加-Dcom.springsource.server.clean=true
VM 参数,您可以通过双击服务器实例,选择“打开启动配置”并单击“参数”选项卡来找到它。
测试类将启动一个 OSGi 框架,加载所有必需的测试 bundle,然后将您的测试代码构建为一个即时 bundle 并对其进行测试。如果您认为这听起来可能很复杂,那么您是对的。亲爱的读者,为了不让您感到失望,我亲自尝试了这一点,老实说,设置它以使所有必需的依赖项都能正常工作有点痛苦。因此,以下是我逐步实现此目的的无痛指南。开箱即用,该测试期望从本地 Maven 存储库获取所有依赖项。您可以将测试配置为使用其他类型的依赖项管理,但为了简单起见(呵呵!),我使用了 Maven。首先,我安装了 m2eclipse,方法是添加http://m2eclipse.sonatype.org/update/
作为更新站点(帮助->软件更新->可用站点->添加站点)。建议取消选中可选的 Maven POM 编辑器,否则会出现一堆未满足的依赖项警告,并且无法安装。src/main/java创建一个名为 StockWatcherServiceTest 的新 Java 项目(使用作为源文件夹)。右键单击该项目,然后选择 Maven->启用依赖项管理以 Maven 化该项目。接下来,创建一个新包(在我的例子中是 com.ben),并将 Spring DM 参考指南中的SimpleOSGiTest
代码复制到该包中的测试类中(请注意,测试框架不允许您使用默认包)。此测试类将帮助我们检查依赖项是否都已正确设置。您会注意到测试仍无法编译,因为我们还没有它需要的依赖项。您需要在pom.xml您会注意到测试仍无法编译,因为我们还没有它需要的依赖项。您需要在文件中指定正确的依赖项。为了节省您数小时的捶胸顿足和亵渎神明,您可以在此处查看我的文件(包含在完整的工作区下载此处)。依赖项正常工作后,看看您是否可以发现示例代码中的拼写错误,添加 java 导入,您现在应该有一个可以编译的测试类。
尝试使用右键单击->运行方式->JUnit 测试来运行测试。如果您收到ClassNotFoundException,那是因为测试在错误的位置查找.class文件。解决此问题的最简单方法是覆盖getRootPath()并返回“file:./target/classes”或 .class 文件生成的任何路径。
希望您现在应该有一个绿色条,这至少证明了测试用例启动了 Equinox 框架,并且能够成功创建一个即时 bundle。现在,我们可以为 StockWatcherServiceLondon bundle 创建我们合适的单元测试。
下一步是使测试依赖 bundle 正确加载和解析。为此,需要将所需的 bundle 安装在本地 maven 存储库中,并在测试用例的getTestBundlesNames()中指定。
我们先来处理 Maven:之前创建的您会注意到测试仍无法编译,因为我们还没有它需要的依赖项。您需要在文件应该已经下载了外部依赖项,但我们需要手动将 com..client.api、com..service.london 和 com.google.gwt bundle 添加到 maven 存储库。我通过 maven 化项目并为每个项目创建一个您会注意到测试仍无法编译,因为我们还没有它需要的依赖项。您需要在文件来为工作区中的 bundle 执行此操作。这里有一些需要注意的地方。首先,要让 Maven 获取MANIFEST.MF文件,您必须在中配置maven-jar-plugin您会注意到测试仍无法编译,因为我们还没有它需要的依赖项。您需要在并将其指向文件的位置。请在此处查看我的。其次,我发现 Spring dm Server 专有的您会注意到测试仍无法编译,因为我们还没有它需要的依赖项。您需要在 Import-Bundle清单条目不起作用,所以我不得不将两个项目都更改为使用多个Import-Package条目。完成这些更改后,打开命令提示符,切换到项目目录并键入mvn install。这将构建插件并将其安装到本地 maven 存储库中。为了安装我们在第 2 部分中构建的 gwt 插件,我使用了以下命令行mvn install:install-file -DgroupId=com.google.gwt -DartifactId=com.google.gwt -Dversion=1.5.3 -Dpackaging=maven-plugin -Dfile=<jar 文件的位置>。此时,值得通过解压缩存储库中的 jar 并检查清单是否正常来检查插件是否已正确构建。
将所有测试 bundle 构建并安装到存储库后,需要在测试用例的getTestBundlesNames()方法中指定它们。语法在参考文档中有描述,我指定了 4 个 bundle:com..client.api、com..service.london、com.google.gwt 和 javax.servlet。
希望此时,您将能够在没有任何实际断言的情况下运行测试,但您应该能够成功地安装和启动所有测试 bundle,而不会出现任何问题。如果它不起作用,一个有用的调试工具是启用 Equinox telnet 控制台并使用它来检查预期的导入和导出是否都正确。如果您收到ClassNotFoundException,则它们可能不正确。启用控制台的方法是覆盖createPlatform()并调用System.setProperty("osgi.console", "9000").
所有 bundle 都正确解析并启动后,您终于可以编写一些断言了!我们需要使用 com..service.london bundle 导出的服务。将上面快速而粗糙示例中的),另一个用于行复制到一个以使用该服务(例如文件中,然后将其添加到测试用例中的getConfigLocations()方法中,该方法返回一个配置文件字符串数组。在这种特殊情况下,我们只是将其设为普通的 OSGi bundle,而不是 Spring Dynamic Module,因此无需将文件放在/spring文件夹中。
在集成测试中,Spring 会将所有 spring 管理的 bean 自动装配到测试用例中,其中有一个合适的 setter,因此添加一个setStockPriceService(StockPriceService s)方法会将 OSGi 服务注入测试类。获得对服务的引用后,您就可以开始对其进行断言……终于可以了!
现在,我意识到这是一个非常冗长的描述,最终是一个简单的概念。坦率地说,理解配置的最佳方法是在此处查看我的 Eclipse 工作区。我详细描述了它,因为看到一个有效的例子是一回事,而理解如何实现它的过程和陷阱则是另一回事。但是,一旦设置好,这是一种以自动化方式集成测试 bundle 服务的极其强大的方法。
从 StockWatcher 开始,它现在不能用了,因为我们从它依赖的 WAR 项目中删除了 API。更改其构建时依赖项,使其引用 StockWatcherServiceAPI 项目(右键单击->构建路径->配置构建路径->项目)。此项目没有运行时依赖项,因此只需完成此操作即可。
StockWatcherWar 现在需要进行一些修改才能使用我们在步骤 2 中创建的 OSGi 服务。目前,它仍然包含我们在步骤 2 中复制到服务包中的服务代码,因此一项工作是删除此代码,并将其替换为委托给 OSGi 服务的调用。然而,这意味着我们需要以某种方式将对服务包的引用传递到类中……这也意味着我们实际上需要从某个地方获取它的实例。让我们进一步分解这个问题。
好的,我们已经知道如何获取服务的引用,因为我们在上面的测试方法中已经这样做了:创建一个 spring 配置文件并添加一个),另一个用于标签。因此,让我们从这里开始。在/META-INF中创建一个新的 Spring Bean 定义(右键单击->新建->Spring Bean 定义),随意命名(例如以使用该服务(例如),并确保选中 osgi 命名空间复选框以包含 schema。添加步骤 3 中的),另一个用于示例代码,您现在就拥有了一个使用 OSGi 服务的 Spring 配置文件。是否应该将此配置文件放入/spring子目录中以使其成为 Spring 动态模块?正确的答案是否定的。我们需要使用它来引导在web.xml中定义的特定类型的 ApplicationContext,因此我们不需要另一个自动为我们创建的 ApplicationContext。
在我们讨论这个问题的同时,编辑 web.xml 并添加以下几行
<context-param> <param-name>contextClass</param-name> <param-value>com.springsource.server.web.dm.ServerOsgiBundleXmlWebApplicationContext</param-value> </context-param> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/META-INF/*.xml</param-value> </context-param>
这将引导一个 dm Server ApplicationContext 并使用 /META-INF 中的所有 xml 文件来配置它。
接下来,我们需要弄清楚如何将这个 Spring 管理的服务实例传递给StockPriceServiceImpl类,换句话说,一种桥接 Spring 和 servlet 世界的方法。一种方法是在ApplicationContextmaven-jar-pluginServletContext中查找并调用getBean()方法。这不是很好,因为它需要我们将 bean 名称硬编码到服务代码中并依赖于依赖查找。一个更简洁的机制是使用 Spring 管理的ServletFilter,将服务实例注入其中,然后使用静态常量进行查找,将其传递给 servlet。这就是我在这里实现的
应该在 StockWatcherWar 项目中创建此类以及StockPriceServiceImpl类。另外,不要忘记在FilterChain的doFilter().
方法中进行委托。如果您一直在仔细按照说明进行操作,由于步骤 1 中的重构,此类或StockPriceServiceImpl此时都无法编译。要解决此问题,您需要将 StockWatcherServiceAPI 作为项目引用添加到 StockWatcherWar(右键单击->属性->项目引用),然后将 com.google.gwt.sample.stockwatcher.client.api 添加到Import-Bundle列表中。进行此更改后,代码现在应该可以编译了。
现在我们已经在 servlet 和 ApplicationContext 之间建立了桥梁,我们所需要做的就是创建一个ServletFilter的实例并将服务注入其中。在/META-INF中创建另一个 spring bean 定义,随意命名(例如stockwatcher-config.xml)。将以下配置复制到其中(假设您使用的 OSGi 服务的 bean 名称是priceService):
<bean id="myFilter" class="com.google.gwt.sample.stockwatcher.server.StockPriceServiceFilter"> <constructor-arg ref="priceService"/> </bean>
最后,我们需要一个名为DelegatingFilterProxy的小魔术。此类将所有传入和传出的 servlet 请求委托给 Spring 管理的ServletFilter,并在web.xml中定义。将此代码复制到您的web.xml中(假设您的ServletFilter的 bean 名称是myFilter)
<filter> <filter-name>myFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>myFilter</filter-name> <servlet-name>StockService</servlet-name> </filter-mapping>
拼图的各个部分终于拼凑在一起了。您会看到在上面的代码中,我们还将DelegatingFilterProxy映射到了 StockService servlet。web.xml配置现已完成。
现在剩下的就是在StockPriceServiceImpl代码中获取服务实例并使用它!
鉴于我们似乎已经考虑到了所有事情,请尝试将 WAR 项目部署到 dm Server,按照步骤 3 中用于快速测试的相同说明进行操作:将 API 项目拖放到服务器上,然后是 London Service 项目,最后是 StockWatcherWar 项目。
您应该会看到以下内容
……(此处省略日志输出,建议保留原文)部署失败了?经过了这一切之后?天哪!这是什么问题?
查看<dm Server 安装目录>/serviceability/trace/com.google.sample.stockwatcher-1/trace.log中的跟踪文件。您应该会看到ClassNotFoundException: org.springframework.web.context.ContextLoaderListener。问题在于,现在我们处于显式导入和导出的世界中,我们需要在 WAR 文件中显式导入为我们提供 Spring 支持的包,因此将以下包添加到Import-Bundle条目中:org.springframework.context、org.springframework.core、org.springframework.beans 和 org.springframework.web。它现在应该如下所示
现在再次尝试部署它。这次,我保证,它*应该*可以工作。在它工作之前,不要继续进行步骤 5。
首先,让我们创建另一个可以交换的股票市场服务。这只不过是一个复制/粘贴的工作。右键单击 StockWatcherServiceLondon 项目,选择复制,然后粘贴。将复制的项目命名为 StockWatcherServiceNewYork。需要编辑的内容如下:
- MANIFEST.MF- 将包名称和符号名称从 London 更新为 NewYork。 -serviceLondon-config.xml- 重命名,但无需更改任何内容 -StockPriceServiceImpl.java- 大幅更改价格,以便很明显我们使用的是不同的股票市场。我只是在每个价格上加了 500。道琼斯加油!
现在是有趣的部分。像以前一样使用 London 股票服务启动 StockWatcherWar,并添加一些股票,以便您可以看到它正在工作
然后右键单击 Servers 视图中的 StockWatcherServiceLondon 条目,然后选择删除。这将从服务器中取消部署 StockWatcherServiceLondon 包。您现在将看到应用程序暂停。对远程服务的调用将阻塞,直到 dm Server 轮询替换服务为止。所以让我们给它一个。将 StockPriceServiceNewYork 项目拖放到服务器上。等待几秒钟……嘿,瞧……该应用程序现在正在使用 New York 服务(请注意价格的显着上涨)。
导出包后,有几种打包方法,然后有几种部署方法。
在打包方面,您可以将它们保留为单独的包,或者可以将它们全部合并到一个 PAR 文件中。PAR 文件看起来像一个包含包的包,此处对其进行了描述。使用 PAR 文件的显著好处是它在其“作用域”中运行其包,因此它与在其他应用程序中运行的其他包完全隔离。但是,如果将应用程序部署为 PAR,则无法在其内部热交换单个包。
确定部署模式后,您可以使用管理控制台上传文件,也可以将它们复制到<dm Server 安装目录>/pickup目录中。第一次执行此操作时,服务器需要已经运行,并且您应该按正确的顺序将包复制到目录中。dm Server 会记住下次启动服务器时的顺序。使用此机制的优点是,您可以通过将单个包移入和移出此目录来热部署或取消部署它们。