1.1.4 开发传统类型的应用
1.1.4.1 B/S
我们首先来看一下,如何基于 OSGi 来开发 B/S 结构的应用。B/S 结构应用程序的开发,可有两个选择:一个是在 OSGi 的框架中嵌入 Http 服务器,另外一个是在 Servlet 容器中嵌入 OSGi 框架。下面分别介绍这两种方式的实现。此外,本节还会给出 Declarative Service 的使用实例。
首先来构想一下这个 B/S 应用的功能。我们提供一个字典服务,用户在浏览器中输入一个单词, 点击提交,给出这个单词的解释。当然,这个例子并不会真正细致地实现这个功能,比如不会真的做一个字典来提供服务,仅仅支持很少的单词的查询。有了这个例子,如果你有兴趣,可以来完善它。
在这个例子中,我们会有四个 Bundle,分别是字典查询响应 Bundle,字典查询接口 Bundle,本地字典服务 Bundle,远程字典服务 Bundle。设计示意图如图 1-18 所示。1.字典查询响应Bundle
提供输入要查询单词的页面,接受用户的查询请求,从 BundleContext 中获取字典服务的Service,调用字典服务的查询接口得到查询结果,并返回结果到页面。
2.字典查询接口 Bundle
对外提供字典查询的接口。
3.本地字典服务Bundle
提供字典查询服务,是从本地的字典中查询结果。
4.远程字典服务Bundle
提供字典查询服务,是从远程的字典中查询结果。
好了,我知道大家已经迫不及待了。下面我们就来动手实现第一个 B/S 结构的应用。
OSGi框架嵌入Http服务器
这一节我们采用把 Http 服务器嵌入到 OSGi 框架中的方法来完成这个字典查询系统的开发。首先我们要准备一下环境。回忆一下,我们在前面讲到 HelloWorld 例子的时候,介绍过环境的准备。在 HelloWorld 的例子中,我们只需要一个系统的 Bundle。现在我们的运行环境要比 HelloWorld 稍微复杂一些,我们需要更多的 Bundle,下面先来准备一下我们的环境。
我们在 Run Configurations 的对话框中创建一个新的 OSGi Framework 的运行配置,在这个配置的 Bundles 中选择下面几个 Bundle : javax.servlet 、 org.apache.commons.logging 、 org.eclipse. equinox.http.jetty 、 org.eclipse.equinox.http.servlet 、 org.eclipse.osgi 、 org.eclipse.osgi.services 和 org.mortbay.jetty,如图 1-19 所示。点击 Run,可以看到有些启动的日志输出。如果看到 osgi>而且没有错误信息,则表明环境已经配置成功;而如果看到类似 Address already in use: JVM_Bind,则说明本机的 80 端口已被占用,由于 Equinox 的 Http Service 实现默认使用的是 80 端口,所以会报这个错,那么我们可以指定 Http Service 使用的端口。我们打开 Run Configurations 中运行配置里面的 Arguments 页签,在 VM arguments 中添加“-Dorg.osgi.service.http.port=8080”就可以设置使用 8080 作为 Http Service 的端口了,本例子中仍采用默认的 80 端口,在正常启动后,输入 ss,回车,可以看到如图 1-20 所示的显示。
说明我们的环境已经准备好了。我们可以打开浏览器,输入 http://localhost/,提示见图 1-21。说明我们的服务器已经正常启动。下面就可以开始 Bundle 的开发工作了。
> 第一步,完成字典查询接口 Bundle 工程。
首先创建名为 DictQuery 的 Plug-in 工程(见图 1-22)。因为这个 Bundle 是为了导出接口,所以这个 Bundle 的 BundleActivator 不做任何的改动。
双击这个 Bundle 工程中 META-INF 下的 MANIFEST.MF 文件,在新的窗口中会显示这个 MANIFEST 的信息(见图 1-24)。可以看到这个窗口下有多个页签,其中 Overview、Dependencies、Runtime、Build 这四个页签是图形化的编辑页签,对应的修改会反映到 MANIFEST.MF 和 build.properties 文件中。MANIFEST.MF 和 build.properties 这两个页签分别直接显示了 MANIFEST.MF 和 build.properties 文件的内容。
下面来完成将接口的 package 从 Bundle 中导出,能够让其他的 Bundle 来使用这个接口的 package。
首先选择 Runtime 页签,然后点击 Exported Packages 中的 Add,在弹出的窗口中选择所创建的 QueryService 所在的 package: org.osgichina.demo.dictquery.query,点击保存就完成导出 Package 的操作了(见图 1-25)。这个时候去看 MANIFEST.MF 文件,其中多了如下一行:
Export-Package: org.osgichina.demo.dictquery.query
到此,我们就完成了字典查询接口 Bundle 的开发了。接着,我们来完成本地字典查询 Bundle 的 开发。
>第二步,完成本地字典查询 Bundle。
可以看到,在自动生成的 Activator 中,我们改动了两处。一个是我们加入了一个类型是 ServiceRegistration 的成员变量 sr,一个是我们在 start 和 stop 方法中分别加入了一行代码,下面我们来看这两行代码的含义。
sr = context.registerService(QueryService.class.getName(),
new LocalDictQueryServiceImpl(), null);
以上两行代码是用 QueryService 的全类名作为注册的 Service 的名称,把新创建的 LocalDictQueryServiceImpl 对象注册成为了一个 Service。而这个 Service 对象将在下面演示如何被使用。
sr.unregister();
以上这行代码是取消注册的 Service。到这里,我们就完成了请求处理 Bundle 的代码编写。
>第三步,完成实现远程字典查询 Bundle。
Bundle 和 LocalDictQuery Bundle 非常地类似,只是工程名称为 RemoteDictQuery,另外为了能够显示区别,这个 Bundle 中提供服务的类的代码有所变化。
实现 QueryService 接口的类的代码如图 1-29 所示。最后,我们完成字典查询响应 Bundle。
>第四步,完成字典查询响应 Bundle。
web 应用服务器那样的 Bundle,就不能像 web 应用直接部署到 web 服务器那样简单了,要通过 HttpService 将 Servlet 及资源文件(像图片、css、html 等)进行注册,这样才可以访问。这里只是一个简单的 Demo,提供一个字典查询的页面和对应的 Servlet。我们在 src 目录下建立一个 page 的目录,在其中编写 dictquery.htm,另外就是实现字典查询响应的 Servlet,由于 Servlet 要继承 HttpServlet,要引用 Servlet API。要引入 javax.servlet 和 javax.servlet.http 两个包,接着,除了要引入 org.osgichina.demo.dictquery.query 这个 package 外,还要一个 org.osgi.service.http package,如图 1-30 所示。
然后,我们可以编写 Servlet 的代码,和普通的 Servlet 的写法没有差别。要解释的是在 doGet 方 法中的这么一段代码:
ServiceReference serviceRef =
context.getServiceReference(QueryService.class.getName());
if(null != serviceRef){
queryService = (QueryService) context.getService(serviceRef);
context 是在创建 Servlet 的时候传入的 BundleContext,要通过这个 context 来获取提供字典查询功能的服务。首先通过 context 获取 Service 的引用,返回的是一个 ServiceReference 对象。然后再通过 ServiceReference 获取 Service 实例。拿到 Service 实例后,就可以调用 Service 的方法来完成字典查询功能了。
另外,在这个字典查询响应 Bundle 中还要做的一件事情就是在 Bundle 启动的时候,把 Servlet 注册到 Http 服务中去。这个代码是在 BundleActivator 中完成的。具体代码可以查看源码。
至此已经完成了字典查询系统的开发。下面来运行一下系统。
在 osgi>提示符下输入 ss,回车,可以看到类似图 1-31 的显示。下面,我们先执行 stop 10(停掉 RemoteDictQuery Bundle),然后输入 test,点击查询。会有如下 显示:
Result is 测试
并且我们可以在 Eclipse 的 Console 中看到如下的输出:
osgi> LocalDictQueryServiceImpl.queryWord called!
接着我们执行stop 11,回车,stop 10,回车。停止LocalDictQuery Bundle,并且启动RemoteDictQuery Bundle,然后在查询页面上输入 sky,点击查询。会有如下显示:
Result is 天空
到这里,我们已经完成了字典查询系统的开发。
1.测试和调试
前面很顺利地完成了字典查询系统的开发和运行。但是在现实生活中,一个系统终的完成和上线,始终离不开测试和调试。对于测试的支持无疑是现在对于框 架的重要考评点。那么基于 OSGi 框架的系统怎么做测试呢?系统的集成测试方法只和系统的结构(B/S、C/S)有关,和框架没什么关系,所以这里就是来看看如何完成基于 OSGi 框架的系统单元测试。
编写单元测试时重要的就两点:
在做单元测试时复杂的部分往往就是设置测试类的依赖,典型的就像 EJB 中的 session bean的测试,由于它要依赖 EJB Container,所以是比较麻烦的。在我们的例子中,已经编写的几个 Service 是没有依赖的情况的,只须测试在某种输入的情况下方法执行的输出和预期的结果是否一致,这就很容易编写了,具体请参见源码中 DictQueryClassic 目录里面的 LocalDictQueryServiceImplTest.java 和 RemoteDictQueryServiceImplTest.java 文件。而在字典查询响应 Bundle,复杂的就是字典查询响应 Servlet 的测试。这个 Servlet 要用到 OSGi 框架的 BundleContext 获取字典查询服务,同时还要依赖 HttpServletRequest 和 HttpServletResponse,在没法实例化类所依赖的环境时,只能采用 Mock 的方法来实现,代码见源码中 DictQueryClassic 目录下的DictQueryServletTest.java。
在这种情况下会发现基于 OSGi 框架的单元测试并不好做,这主要是因为在之上的例子中对于服务的获取、注册都是采用 BundleContext 来完成的(也就意味着要依赖 OSGi 框架,与 HttpServletRequest 需要 Mock 和 OSGi 框架没什么关系)。在后续 Declarative Services 的章节中介绍采用 DI 方式来实现时,就无须 Mock BundleContext 来获取服务了,到时会将测试这部分的代码进行重写。
调试也是系统开发关注的重点,由于可以在 Eclipse 中启动基于 Equinox 开发的系统,那么一切都不是问题。和普通 Java 工程进行调试的方法没有任何区别,设置断点,Debug,就 OK 了。在运行到断点对应的代码时就进入熟悉的调试视图了(见图 1-33)。2.应用的部署
我们已经完成了字典查询整个应用的开发、运行、调试和测试工作。不过这些都是在 Eclipse 这个开发工具中完成的,我们可不能给客户一个要客户在 Eclipse 上运行的应用,所以下面来看一下如何部署 OSGi 应用。
>第一步,创建独立的 Equinox 运行环境。
在硬盘上创建一个 osgidemo 目录,从 Eclipse 的 plugins 目录复制 org.eclipse.osgi3.4.3.R34x v20081215-1030.jar(在不同版本的 Eclipse 中,这个 jar 包的 org.eclipse.osgi后面的部分会有所不同)到这个 osgi_demo 目录。修改这个 jar 包的名称为 equinox.jar,然后在相同目录下编写一个 run.bat,其内容如下:
java -jar equinox.jar -console
双击 run.bat,如果出现 osgi>的提示,则说明启动已经成功了。输入 ss 命令然后回车,这个时候 会看到只有一个 ACTIVE 状态的 system bundle。
>第二步,导出各个 Bundle 工程为 jar。
以复杂的 DictQueryWeb 为例,首先打开 MANIFEST.MF,选择 Runtime 页签,设置 Classpath (见图 1-34)。接着,选中 DictQueryWeb 工程,点右键,选择 Export,然后选中弹出对话框中的 Deployable plug-ins and fragements(见图 1-36)。
在进入的 Deployable plug-ins and fragments 窗口中已经默认选择了 DictQueryWeb 这个bundle,然后选择 Destination 标签页,设置一个有效的 Directory,然后点击 Finish,在设置的目录中可以看到一个 plugins 目录,在 plugins 目录中就有 DictQueryWeb_1.0.0.jar 这个 bundle 了。
按照相同的方法可以导出其他的几个 bundle,也可以一次性地把几个 bundle 都导出来(见图 1-37)。>第三步,安装各 Bundle 到 Equinox 中。
首先在 osgidemo 目录下创建一个 bundles 目录,然后将第二步生成的三个 bundle 复制到 bundles 目录下,此外,我们要从 Eclipse 的 plugins 目录中把我们需要的 javax.servlet2.4.0. v200806031604.jar、 org.eclipse.equinox.http.servlet1.0.100.v20080427- 0830.jar 、 org.eclipse.equinox. http.jetty1.1.0.v20080425.jar 、 org.mortbay.jetty5.1.14. v200806031611.jar 、 org.apache.commons. logging1.0.4.v20080605-1930.jar、org.eclipse. osgi.services_3.1.200.v20071203. jar 这几个 Jar 文件复制到 Bundles 目录中。
有两种办法可将 Bundle 安装到 Equinox 中:
可见,目前我们安装后的 10 个 Bundle 都已经是 INSTALLED 的状态。下面让我们来启动这些 Bundle 。首先来启动系统的 Bundle ,依次启动 javax.servlet 、 org.apache.commons.logging 、 org.eclipse.osgi.services、org.mortbay.jetty、 org.eclipse.equinox.http.servlet 和 org.eclipse.equinox.http. jetty,然后启动我们自己开发的 Bundle。可以在 osgi>提示符下输入 ss,会有如图 1-39 所示的输出。
这个时候,可以通过浏览器来测试我们的应用了。
后输入 exit,就可以退出系统。以后只须双击 run.bat 就可以完成系统的启动。经过这样的步骤,就形成了单独运行的 OSGi 系统。在 osgi_demo 目录下创建 configuration 目录,在 configuration 目录下创建 config.ini 文件,这个文件内容如下:
osgi.noShutdown=true
osgi.bundles=reference\:file\:bundles/javax.servlet_2.4.0.v200806031604.jar@start ,reference\:file\:bundles/org.apache.commons.logging_1.0.4.v20080605-1930.jar@sta rt,reference\:file\:bundles/org.eclipse.osgi.services_3.1.200.v20071203.jar@start ,reference\:file\:bundles/org.mortbay.jetty_5.1.14.v200806031611.jar@start,refere nce\:file\:bundles/org.eclipse.equinox.http.servlet_1.0.100.v20080427-0830.jar@st art,reference\:file\:bundles/org.eclipse.equinox.http.jetty_1.1.0.v20080425.jar@s tart,reference\:file\:bundles/DictQuery_1.0.0.jar@start,reference\:file\:bundles/ LocalDictQuery_1.0.0.jar@start,reference\:file\:bundles/RemoteDictQuery_1.0.0.jar
,reference\:file\:bundles/DictQueryWeb_1.0.0.jar@start osgi.bundles.defaultStartLevel=4
这样,双击 run.bat 就能够完成整个系统的启动。
接下来,介绍使用 Declarative Service 来实现的字典查询系统。
Declarative Service版本的实现
这一节,我们将采用 Declarative Service 的方式来实现前面的字典查询系统。在第 1 章,我们对 Declarative Service 已有一个初步的介绍。相比于通过 BundleContext 获取 Service 这样的方式,Declartive Service 更像是一种 Dependency Injection 的方式。下面我们来准备一下环境,首先下载 org.eclipse.equinox.ds 包(https://www.osgi.org/repository /org.eclipse.equinox.ds_1.0.0.v20060828.jar),下载完毕后放入 Eclipse 的 plugins 目录中,重新启动 Eclipse。下面来修改我们之前的字典查询系统的代码。
1.重构服务发布实现
在我们的例子中,LocalDictQuery Bundle 和 RemoteDictQuery Bundle 是通过在 BundleActivator 中主动注册服务来提供服务的。这里要重构这两个 Bundle 的代码。
<?xml version="1.0" encoding="UTF-8"?>
<component name="DictQueryService">
<implementation class="org.osgichina.demo.localdictquery.impl.
LocalDictQueryServiceImpl"/>
<service>
<provide interface="org.osgichina.demo.dictquery.query.
QueryService"/>
</service>
</component>
这个是 LocalDictQuery Bundle 的 component.xml,RemoteDictQuery Bundle 的 component.xml 是类似的写法,只是 implementation 的 class 有所不同。另外,我们不在 RemoteDictQuery Bundle 的 MANIFEST.MF 中加入 Component 的设置,我们只希望系统中有一个 DictQuery 服务。
Service-Component: OSGI-INF/component.xml
这就完成了服务发布的重构,下面来看一下服务引用的重构。
2.重构服务引用实现
我们的例子中只有字典查询响应 Bundle 引用了服务,只须修改这一个工程。
<?xml version="1.0" encoding="UTF-8"?>
<component name="DictQueryServlet">
<implementation class="org.osgichina.demo.dictqueryweb.servlet.
DictQueryServlet"/>
<reference name="QueryService" interface="org.osgichina.demo.dictquery.query.QueryService" bind="setQueryService" unbind="unsetQueryService" policy="dynamic" cardinality="0..1"/> <reference name="HttpService" interface="org.osgi.service.http.HttpService" bind="setHttpService" unbind="unsetHttpService" policy="dynamic"/>
</component>
完成了重构,就可以启动系统了,须要注意的是,在 Run Configurations 中要引入 org.eclipse.equinox.ds bundle,并且设置这个 bundle 的 start level 为 2,让 ds 这个 bundle 先启动。然后我们可以通过浏览器来测试一下系统的功能。
现在 Servlet 重构后不须要再通过 BundleContext 获取 QueryService 了,对于 Servlet 的测试代码也无须去 Mock ServiceReference 和 BundleContext 对象了。新的测试代码请参看源码中的 DictQuery_DS 目录下的 DictQueryServletTest.Java。
我们已经完成了 OSGi 框架中嵌入 Http 服务器的例子,也介绍了 Declarative Service 的用法。下 面我们简单看一下如何通过把OSGi框架嵌入Servlet容器中的方式完成基于OSGi的Web应用的开发。
Servlet容器嵌入OSGi框架
在 Equinox 中提供了另外一种方式,是在 Servlet 容器中嵌入 Equinox。在 Equinox 的网站上,可以下载到 bridge.war(http://www.eclipse.org/equinox/server/downloads/bridge.war),也 可以自己从 CVS 下载代码来编译新的版本。我们把这个 bridge.war 部署到 Servlet 容器中,就可以在控制台上使用Equinox 的命令了。启动后的显示如图 1-40 所示。
从中可以看到熟悉的osgi>的提示,我们可以在osgi>提示符下安装使用BundleContext注册Service 的版本系统的 Bundle,然后启动已经安装的 Bundle。我们可以从浏览器上使用我们的系统,但是在提交后会出现问题。原因是什么呢?因为现在我们的系统是在 bridge 这个 Web 应用下的,而我们的 HTML 页面却是提交到了/demo/dictquery,这样是不行的,一个办法是修改提交的地址,另外一个办法是把 bridge.war 这个包解压后直接放到 Tomcat 安装目录的 webapps 下的 ROOT 目录中。
在这两种 B/S 的开发方式中,Equinox 是推荐使用在 Equinox 中嵌入 HttpServer 的方式来进行的。所以在 Servlet 容器中嵌入 Equinox,我们就不再花过多的篇幅来介绍了。