在 Part1 中,我们提到了企业级 OSGi 制定了一系列的规范来与 JavaEE 集成,其中,最具代表性的规范是 OSGi WEB 应用程序规范,这部分将带领大家深入理解 OSGi WEB 应用程序规范和 GlassFish OSGi/WEB 容器。本文将分成以下几个部分:
- 理解 OSGi WEB 应用程序规范
- 构建一个 OSGi WEB 应用程序
- 使用 GlassFish 4 部署 OSGi WEB 应用程序
- 深入剖析 GlassFish OSGi/WEB 容器
- 思考
理解 ****OSGi WEB应用程序规范
为什么需要 ****OSGi WEB
在信息和网络发达的今天,WEB 应用程序已经非常得流行和普遍,一些任务关键型 (Mission-Critical) 的 WEB 应用程序每天都在高负荷地运行,很少有中断,因为一次不经意的中断可能造成数据的大规模丢失,以至损失大量的资金而造成严重的后果。这些任务关键型的 WEB 应用往往出现在证券和股票等相关的金融行业。现在,我们开始考虑一个场景: 几个星期或者几个月甚至几年后,WEB 应用的客户或者提供商希望在 WEB 前端增加一些新的模块或功能,但是,为了增加这些新的模块,我们不能停止 WEB 应用,而且也不希望再次重构或者改变 WEB 应用的现有架构和模块。这听起来不可思议,对于这样一个场景,至少应该停止应用程服务的实例吧。但是,客户不会答应。另一方面,在当今大数据的时代,每一秒钟都会有大量的数据进入我们的应用之中。那么,如何解决这样的场景?
一个可行的答案是: 使用 OSGi WEB 构建我们的应用。
WAB-OSGi WEB 的核心
简单地说,OSGi WEB 应用程序规范 (chapter 128 in the OSGi Enterprise Release 5 Specification[1]) 定义了 OSGi WEB 的全部内容。对于 OSGi WEB 应用程序,典型情况下,它由一个 WEB 应用程序 Bundle(即 Web Application Bundle,简称为 WAB)所构成。
因此,首先我们需要理解 WAB 以及和 WAR 的区别。
WAB 简述
在 Part1 中,我们已经提到 Bundle 是 OSGi 中的基本部署和管理实体。所以,WAB 首先是一个 Bundle,必须提供成为 Bundle 的 OSGi 元数据 (如, Bundle-SymbolicName, Bundle-Version…),其次,WAB 与 JavaEE 中的 WAR 一样,依然是服务于 WEB 应用程序,能够使用 Servlet 2.5 或更高版本的 Servlet 规范,因此,WAB 必须包含可访问的 WEB 内容,具体的说,Java Servlet 规范定义了一个 WEB 应用程序的结构并定义了一个基于 JAR 的文件格式(WAR),WAB 必须包含 WAR 中的静态和动态的内容。
进一步地,要成为一个 WAB,需要在 MANIFEST.MF 文件中通过 Import-Package 来描述它的依赖,例如: 通过导入 javax.servlet 来使用 Servlet 的功能,另外,如果需要向外界提供服务,它也要通过 Export-Package 来导出服务所在的包。
我们能够通过不同的方式来安装 WAB,例如,通过支持企业级 OSGi 的应用服务器所提供的命令行控制台 (如,GlassFish Admin CLI),也可以通过程序的方式调用 OSGi 底层 API 来安装 WAB(如,BundleContext.installBundle)。无论哪一种方式,WAB 安装后,它的生命周期管理就像 OSGi 运行时的其他 Bundle 一样。只不过 WAB 的生命周期被一个 Web Extender 跟踪,一旦 WAB 准备服务 WEB 请求时,Web Extender 需要将 WAB 中可访问的 WEB 内容部署到 WEB 运行时。以后当 WAB 不再服务 WEB 请求时,Web Extender 也需要将这些可访问的 WEB 内容从 WEB 运行时卸载掉。
关于 WAB 的安装,有一点需要额外说明,一个 WEB 应用程序能够在开发阶段通过工具 (例如, Maven 插件) 被打包成 WAB 然后进行安装,或者这个 WEB 应用程序能够在 Bundle 安装阶段通过 Web URL Handler 对标准 WAR 进行转换来透明地创建 WAB。GlassFish 4 已经实现了后一种机制,我将在后续章节详细阐述。
关于 Web Extender 和 Web URL Handler,它们都是 OSGi WEB 容器的一部分,我们将在后面章节详细阐述。
从上面的叙述,我们已经看到了安装 WAB 与安装普通 Bundle 的明显的不同之处: 除了安装 WAB 到 OSGi 运行时,还需要将 WAB 中可访问的 WEB 内容部署到 WEB 运行时。关于这一点,OSGi WEB 应用程序规范定义了 WAB 的生命周期状态图,
图 1: WAB 的生命周期状态图
摘自: OSGi Enterprise Release 5 Specification
我们将在后续章节中深入阐述图 1 中的每个阶段。
WAB 定义
WAB 本身就是一个 OSGi Bundle,因此,对于标准 OSGi Bundle 的定义同样适用于 WAB,但是,WAB 与标准 OSGi Bundle 本质的区别在于: WAB 需要在 MANIFEST.MF 中定义 Web-ContextPath 属性。Web-ContextPath 属性定义了这个 WEB 应用程序访问的上下文路径 (Context Path)[2],在 WEB 服务器上,这个 WEB 应用程序中所有可访问的资源都要相对于这个上下文路径。例如, 如果在 MANIFEST.MF 定义了以下 Web-ContextPath 属性,
Web-ContextPath: /uas
那么访问这个 WEB 应用程序的 URL 总是相对于 http://host:port/uas ,需要注意的是: Web-ContextPath 属性的值总是以斜杠’/’开始。
当安装 WAB 时,除非 Web-ContextPath 属性出现在 MANIFEST.MF 中且 Web-ContextPath 的值是一个有效的值,否则,Web Extender 会认为这不是一个 WAB,而视为一个普通的 Bundle。
WAB 结构和相关的 OSGi 元数据
上面已经看到,除了标准 OSGi 元数据,WAB 必须要在 META-INF/MANIFEST.MF 文件中定义 Web-ContextPath 属性。例如,以下是一个 WAB 的结构,
图 2: 一个 WAB 的结构示例
这个 WAB 定义的 OSGi 元数据如下所示,
图 3: 图 2 的 WAB 的 OSGi 元数据示例
在图 2 中,我们定义了一个 WAB,这个 WAB 中有一个 Servlet,被放在了 WEB-INF/classes 目录下,而且这个 WAB 有两个内部依赖,lib1.jar 和 lib2.jar。当安装 WAB 时,为了使这些动态的内容都能够被 Web 服务器访问到,我们就必须在这个 WAB 的 MANIFEST.MF 中按照一定的规则指定 OSGi 元数据,也就是图 3 所示的那样,
- 指定一些必须的属性包括 Bundle-ManifestVersion、Bundle-SymbolicName、Bundle-Version。Bundle-Name 是可选的,这是一个历史遗留的属性,你可以不用指定它,但是我通常也会指定这个属性,因为,Bundle-Name 属性的值可以用来反映 Bundle 的用途。
- 指定 Import-Package 属性,因为这个 WAB 正在使用 Servlet,所以我们导入了 Servlet 相关的包。
- 指定 Bundle-ClassPath 属性,这个属性非常重要,它定义了如何加载内部的依赖和 WAB 自身的类,我们把 WEB-INF/classes/ 放在 WEB-INF/lib/lib1.jar 和 WEB-INF/lib/lib2.jar 的前面,这样做是为了和传统 WAR 文件搜索类的顺序一致,简单地说,优先搜索 WAB 自身的 Class,然后再搜索依赖的库文件。
- 指定 Web-ContextPath 属性。
通过对 MANIFEST.MF 追加 OSGi 元数据,也再次说明了 WAB 使用 OSGi 生命周期和类 / 资源加载规则而不是标准 JavaEE 环境的加载规则,这点至关重要。
WAB 的生命周期
在图 1 中已经提到了 WAB 的生命周期,仔细地与标准 OSGi Bundle 的生命周期比较一下,你会发现,WAB 的生命周期多了四个阶段 (DEPLOYING、DEPLOYED、UNDEPLOYING 和 UNDEPLOYED)。
当一个 WAB 处于 DEPLOYED 阶段时,它已经做好了准备来服务即将到来的 WEB 请求。处于 DEPLOYED 阶段也意味着这个 WAB 或者处于 ACTIVE 状态,或者处于 STARTING 状态 (因为有一个懒惰的激活策略)。关于懒惰的激活策略,在《OSGi In Action》一书第 9.3 节“Starting bundles lazily”有精彩的介绍。
对于具有懒惰的激活策略的 WAB 来说,Web Extender 应该确保当服务 WEB 的静态内容 (如图像资源、HTML 和 CSS 等) 时不能改变该 WAB 所处的状态,即仍然使它处于 STARTING 状态。
从图 1 中,我们能够清楚地看到,为了让 WAB 能够服务即将到来的 WEB 请求,WAB 需要从 DEPLOYING 迁移到 DEPLOYED 阶段,Web Extender 必须部署 WAB 中的 WEB 应用程序相关的类和资源到 Web 运行时。具体地,
- 等待 WAB 处于 ACTIVE 状态或 STARTING 状态
- 发送 org/osgi/service/web/DEPLOYING 事件
- 验证 Web-ContextPath 属性的值没有和其他已经被部署的 WEB 应用程序的上下文路径冲突,也就是说保证上下文路径的唯一性。如果有冲突,那么部署 WAB 失败,Web Extender 应该记录下部署失败的日志。
- 如果 3 的验证通过,那么按照以下的顺序,Web 运行时开始处理部署相关的细节,如果 web.xml 存在的话,它也会处理 web.xml 中的内容。
- 为这个 WEB 应用程序创建一个 Servlet 上下文
- 初始化配置的 Servlet 事件侦听器
- 初始化配置的应用程序过滤器等
- 注册 Servlet 上下文作为 OSGi 服务
- 发送 org/osgi/service/web/DEPLOYED 事件通知当前的 WAB 已经准备好了,可以服务 WEB 请求。
如果在 org/osgi/service/web/DEPLOYED 事件发送前的任何时候有异常或错误发生,那么 WAB 的部署将失败。
图 1 中我们也能够发现,一旦不再需要该 WAB 服务 Web 请求时,那么该 WAB 需要从 DEPLOYED 经过 UNDEPLOYING 迁移到 UNDEPLOYED 阶段(UNDEPLOYING 是一个暂态)。
有几种方法能够使 WAB 处于 UNDEPLOYED 阶段,
方法 1: 停止 WAB
一旦接收到 WAB STOPPING 事件,Web Extender 必须立刻从 Web 运行时中 _undeploy_ Web 应用程序资源。Undeploy 的主要步骤如下:
- 发送 org/osgi/service/web/UNDEPLOYING 事件通知 Web 应用程序资源将被 undeploy。
- 从 OSGi 注册表中移去 Servlet 上下文。
- Web 运行时必须让该 Web 应用程序停止服务请求。
- Web 运行时必须清理所有 Web 应用程序相关的资源,如占用的 JAR,以及清理 ClassLoader 避免内存泄漏等。
- 发送 org/osgi/service/web/UNDEPLOYED 事件。
方法 2: 卸载 (Uninstall)WAB
除了停止 WAB,也能够通过从 OSGi 运行时中卸载 WAB 来 undeploy 对应的 Web 应用程序资源,undeploy 步骤和方法 1 一样。
方法 3:停止 Web Extender
当停止 Web Extender 时,所有被部署的 WAB 都将被 undeploy,但是,尽管 WAB 被 undeploy 了,它任然处于 ACTIVE 状态。
从以上可以得出,WAB 生命周期的四个特有状态不同于标准 OSGi Bundle 的状态,WAB 生命周期的特有状态并不受 OSGi 生命周期层控制,不是标准的 OSGi 状态,这些特有的状态仅仅由 Web Extender 控制。
关于 WAB 生命周期,在“深入剖析 GlassFish OSGi/WEB 容器”中将再次阐述。
另外,当你阅读 OSGi Enterprise Release 5 Specification 时,特别要注意不能将 Uninstall 和 Undeploy 混为一谈,尽管在一些场合下这两个术语都能够理解为“卸载”。
OSGi Web 容器
最后我们来谈一下 OSGi Web 容器,在上面的章节中我们已经多次提到了 Web Extender,Web 运行时以及 Web URL Handler。这些实体构成了 OSGi Web 容器,而 OSGi Web 容器是 OSGi Web 规范的实现。根据 OSGi Web 规范,OSGi Web 容器由以下三个实体构成:
-
Web Extender
验证是否为 WAB 并且跟踪 WAB 的生命周期,同时负责部署 WAB 到 Web 运行时以及 undeploy 一个被部署的 WAB。
-
Web 运行时 Web 应用程序运行时环境,对于 GlassFish 来说,Web 运行时基于 Tomcat Catalina。
-
Web URL Handler 一个 URL Stream Handler,这个 URL Stream Handler 能够处理 webbundle: scheme,这个 scheme 能够被用来转换以及安装 WAR 到 OSGi 运行时中,GlassFish 4 提供了一个新的特性,即通过实现这个 URL Stream Handler 在部署时自动转换和安装 WAR 到 OSGi 运行时。
构建一个 OSGi WEB 应用程序
回到开始提出的问题场景,即如何在不停止 JVM 的情况下,构建一个动态的 Web 应用程序?
以下的 Sample 应用程序源于我曾经调查的一个 GlassFish 问题 [3],先看一下需求,
问题场景
我们希望构建这样一个 Web 应用程序,当启动这个 Web 应用程序时,没有任何 Web 模块被加载,界面显示“No modules available.”,然后,当部署一个 Web 模块后,在浏览器上点击刷新按钮 (不重启应用程序),这个 Web 模块随即出现在界面上。
开发工具
在本文中我将使用如下的一些工具来构筑开发环境,其中,根据个人的使用习惯,你也可能使用 NetBeans 或 IntelliJ IDEA。
- JavaSE7
- Maven 3.0.4
- Eclipse Kepler
应用程序架构
下面详细地说明一下图 4,
- 将要创建的应用程序分成 Web 前端,存放模块接口的 Core,以及实现模块接口的各个模块。Web 前端采用 JSF 2+CDI,也就是使用 JavaEE CDI Bean 与 JSF 页面进行绑定。应用程序的每个 Web 模块都需要实现 Core 中的模块接口。
- Web 前端以 WAB 方式打包,Core 和每个模块打包成标准 OSGi Bundle。
- 每个模块有一个 OSGi Activator,一旦部署模块到 GlassFish OSGi 运行时,将首先执行模块的 Activator 方法来注册模块服务以便让 Web 前端的服务侦听器能够获取到相应的模块服务。
- 一旦 Web 前端的服务侦听器 (ServiceListener) 发现有新的模块被注册,那么该模块将被添加到应用程序 Bean 的模块集合中,类似的,一旦发现既有的模块从 OSGi 服务注册表中删除,那么应用程序 Bean 的模块集合将移除该模块。
构筑开发环境来创建应用程序
我们将使用 Maven 来一步一步地创建一个多模块的工程,我推荐使用如下的方式来创建多模块的工程,关于这种方式的详细说明,你能够参考 [4]。
假设我使用 Windows 平台来创建 Sample 应用程序。
- 创建 Sample 应用程序的 Parent Pom 文件
运行 Windows 命令行,在当前的工作目录下,执行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish.wab.sample -DarchetypeArtifactId=maven-archetype-site-simple
成功执行后,你会发现在当前工作目录下创建了一个“glassfish.wab.sample“目录,并且有一个 pom.xml 文件,这个文件就是 Sample 应用程序的 Parent Pom 文件。
2. 配置 Sample 应用程序的 Parent Pom 文件
打开 Sample 应用程序的 Parent Pom 文件,放入以下的内容,
<build> <finalName>${project.artifactId}</finalName> <pluginManagement> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <!-- 2.2.0 and above have new bnd which has wab instruction. 2.3.4 has few important bug fixes. --> <version>2.3.4</version> <extensions>true</extensions> <configuration> <supportedProjectTypes> <supportedProjectType>ejb</supportedProjectType> <supportedProjectType>war</supportedProjectType> <supportedProjectType>bundle</supportedProjectType> <supportedProjectType>jar</supportedProjectType> </supportedProjectTypes> <instructions> <!-- Read all OSGi configuration info from this optional file --> <_include>-osgi.properties</_include> <!-- No packages are exported by default. Having any pattern is dangerous, as the plugin will add any package found in dependency chain that matches the pattern as well. Since there is no easy way to have an include filter for just local packages, we don't export anything by default.--> <Export-Package>!*</Export-Package> </instructions> </configuration> … </plugin> … </plugins> </build> <dependencyManagement> <dependencies> <dependency> <groupId>org.osgi</groupId> <artifactId>org.osgi.core</artifactId> <version>4.2.0</version> <scope>provided</scope> </dependency> … </dependencies> </dependencyManagement> <dependencies> <!-- Add the the following dependencies to every module to save user from adding them to every one. --> <dependency> <groupId>org.osgi</groupId> <artifactId>org.osgi.core</artifactId> </dependency> … </dependencies>
以上内容基于 https://svn.java.net/svn/glassfish~svn/trunk/fighterfish/sample/parent-pom/pom.xml ,完整的 POM 文件内容,请参照 https://github.com/tangyong/GlassFishOSGiJavaEESample/blob/master/glassfish.wab.sample
/pom.xml 。
你一定会问,为什么要放入这些内容?以下是几个重要的原因:
- Maven 工程的 POM 文件有很好的继承关系,就像面向对象的类设计一样,将子工程需要的一些共通插件 (plugin) 和共通的依赖 (dependency) 放入到 Parent POM 文件中总是很好的做法。
- 为了构建 WAB,我们放入 maven-bundle-plugin[5],maven-war-plugin[6] 以及为了编译 Java 源文件所需要的 maven-compiler-plugin[7] 等。这里,需要说一下 maven-bundle-plugin,这个插件的目的是将工程打包成标准 OSGi Bundle 的文件格式,其内部使用了 bnd[8],bnd 是由 OSGi 联盟前主席 Peter Kriens 创建,用来简化开发 OSGi Bundle 的痛苦。从上面的 maven-bundle-plugin 的配置看,有一个地方需要特别说明:
<instructions> <_include>-osgi.properties</_include> <Export-Package>!*</Export-Package> </instructions>
上述的指令中,通过“_include”标签指定了一个配置 OSGi 元数据的文本文件,这个文本文件的位置相对于当前 Maven 工程的根目录 (你也可以自行配置它的位置),osgi.properties 中的内容是一组指定 OSGi 元数据的规则,以下是一个 osgi.properties 的示例:
Export-Package: \ sample.foo; \ sample.bar; version=${project.version} Import-Package: \ sample.car;resolution:=optional, \ * Bundle-SymbolicName: \ ${project.groupId}.${project.artifactId} …
关于详细的指定规则,请参见 [9]。
这里也要特别说明一下,我们使用 Maven War 插件的 2.4 版本而不是 2.1 版本,因为 2.1 版本在 Windows 平台上打包时,会生成两个 web.xml 文件。这个问题同样出现在 fighterfish 子工程的 Sample Parent POM 中,我将很快修复它。
Export-Package
在上面的 maven-bundle-plugin 的配置中,还出现了
3. 创建 Core 子工程
从 Windows 命令行进入“assfish.wab.sample“目录,执行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish.wab.sample.core
成功执行后,你会发现在“glassfish.wab.sample“目录下创建了一个“glassfish.wab.sample.core“目录,进入“glassfish.wab.sample.core“目录并打开 pom.xml 文件,你会发现以下内容已经自动被添加了。
<parent> <groupId>cn.fujitsu.com.tangyong</groupId> <artifactId>glassfish.wab.sample</artifactId> <version>1.0-SNAPSHOT</version> </parent>
然后,在“glassfish.wab.sample.core“目录下创建一个 osgi.properties 文件,内容如下:
Export-Package={local-packages}; version=${project.version}
这样的话,当构建最终 Bundle 时,Bundle 将导出内部的带有工程版本的包。
4. 创建 Web 客户端子工程
类似 3,执行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish .wab.sample.web -DarchetypeArtifactId=maven-archetype-webapp
成功执行后,你会发现在“glassfish.wab.sample“目录下创建了一个“glassfish.wab.sample.web“目录。然后,新建 src/main/java 和 src/main/resources/META-INF 目录。默认地,这两个目录不会被创建。
接着,在“glassfish.wab.sample.web“目录下创建一个 osgi.properties 文件,内容如下:
Web-ContextPath:/wabsample
我指定了这个 WAB 的 Web 上下文路径为 /wabsample,你也可以自行修改为其他的值。
5. 创建 WEB 模块 1 子工程
类似 4,执行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish .wab.sample.module1 -DarchetypeArtifactId=maven-archetype-webapp
成功执行后,你会发现在“glassfish.wab.sample“目录下创建了一个“glassfish.wab.sample.module1“目录。
然后,打开该工程的 pom 文件,添加“glassfish.wab.sample.core“依赖声明,
<dependency> <groupId>cn.fujitsu.com.tangyong</groupId> <artifactId>glassfish.wab.sample.core</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 创建 WEB 模块 2 子工程
类似 5,这里就跳过。
7. 设置开发环境
一旦这些 Maven 子工程工程创建成功,我们将进行开发环境的设置,进入编码阶段,以下的步骤描述了如何将 Maven 集成到 Eclipse。假定我的 Eclipse 工作空间 (Workspace) 是“E:\QCON\WS“。
-
修改 Kepler 中的 M2_REPO 变量
修改 Kepler 中的 M2_REPO 变量的目的是为了设置 M2_REPO 的值为你机器上的 Maven 本地仓库 (Local Repository)。默认地,Kepler 中的 M2_REPO 变量的值为~/.m2/repository。具体的修改步骤可以参照 [10]。
-
为 Maven 工程创建 Eclipse 相关的文件 (如,.project 文件)
从 Windows 命令行进入“glassfish.wab.sample“目录,执行以下命令:
mvn eclipse:eclipse
然后将“glassfish.wab.sample“工程连同子工程导入到 Eclipse 中。如果一切成功的话,在 Eclipse 中,应该看到类似如下的画面。
图 5: 成功导入到 Eclipse 的 Sample 应用程序结构示意图
应用程序核心逻辑
-
glassfish.wab.sample.core
新建一个名为“Module“的接口, 该接口的定义如下:
public interface Module { public String getModuleName(); public URL getResource(String path); public String getAboutPage(); }
-
glassfish.wab.sample.web
Web 子工程的核心是 ApplicationBean 类,这也是一个 CDI Bean,并且和 JSF 页面绑定在一起,成为了 JSF 托管 Bean(Managed Bean)。以下是 home.xhtml 页面中与 ApplicationBean 相关的内容,
<h:body> <h:panelGroup layout="block" rendered="#{not empty applicationBean.modules}"> Modules: <br/> <ui:repeat value="#{applicationBean.modules}" var="module"> <h:panelGrid columns="1"> <h:link outcome="#{module.moduleName}#{module.aboutPage}" value="# {module.moduleName}" /> </h:panelGrid> </ui:repeat> </h:panelGroup> <h:panelGroup layout="block" rendered="#{empty applicationBean.modules}"> No modules available. </h:panelGroup> </h:body>
其中,#{applicationBean.modules}是 JSF 表达式语言,通过这个表达式,能够获取到 ApplicationBean 类实例中的 modules 变量的值。在设计这个页面时,我们通过 ui:repeat 标签动态地追加 <h:panelGrid>,一旦有新的模块到来或者既有模块被移除,ApplicationBean 类实例中的 modules 变量的值将发生改变,然后,当刷新浏览器时,JSF 页面将呈现出不同的内容。
那么,ApplicationBean 类实例是如何跟踪到模块的注册和移除的呢?首先,让我们看一下 ApplicationBean 类的定义:
@Named @ApplicationScoped public class ApplicationBean { @Inject private BundleContext bc; @Inject private ModuleListener moduleListener; private List<Module> modules = new ArrayList<Module>(); @Inject public void initialize(ServiceTracker st) { bc.addServiceListener(moduleListener); loadServices(st); } public void afterAddModule(Module module) { System.out.println("Module added."); modules.add(module); } public void beforeRemoveModule(Module module) { System.out.println("Module removed"); modules.remove(module); } public List<Module> getModules() { return modules; } private void loadServices(ServiceTracker st) { ServiceReference[] srs = st.getServiceReferences(); if (srs != null) { for (ServiceReference sr : srs) { Module m = (Module) bc.getService(sr); modules.add(m); } } } }
以上定义中,moduleListener 扮演了重要的作用,moduleListener 是 org.osgi.framework.ServiceListener 的一个实现,ServiceListener 的作用是用来跟踪 OSGi 服务的注册,更新以及卸载。afterAddModule 和 beforeRemoveModule 作为回调方法被 moduleListener 调用,具体地,moduleListener 中注入了 ApplicationBean 实例,一旦有新的模块到来,moduleListener 就会通过 ApplicationBean 实例来调用 afterAddModule 方法,如果既有的模块被移除,那么就调用 beforeRemoveModule 方法。
在 glassfish.wab.sample.web 中还有一些其他的类,因为篇幅关系,就不一一叙述了,详细地内容请参见: https://github.com/tangyong/GlassFishOSGiJavaEESample/tree/master
/glassfish.wab.sample/glassfish.wab.sample.web
-
glassfish.wab.sample.module1
模块 1 很简单,只有两个类,实现 Module 接口的 Module1 类和 BundleActivator 的实现类 Activator。我们必须要追加一个 BundleActivator 的实现类以便模块 1 在启动时能够将自己注册到 GlassFish OSGi 运行时的服务注册表中。
-
glassfish.wab.sample.module2
类似于模块 1,这里就省略跳过。
完整的 Sample 应用程序,请从 https://github.com/tangyong/GlassFishOSGiJavaEESample 中下载。
使用 GlassFish 4 部署 OSGi WEB 应用程序
一旦我们构建完 Sample 应用程序,就将使用 GlassFish 4 来部署它。
安装和启动 GlassFish
首先,你需要从以下链接下载一个 GlassFish 4 的安装 zip 包。然后解压缩这个 zip 包到本地的文件系统。
http://download.java.net/glassfish/4.0/release/glassfish-4.0.zip
然后,通过以下命令,启动 GlassFish 的 domain,默认地,GlassFish 会为你创建好一个 domain。假设解压缩后的 GlassFish 所在的目录用 $GlassFish_HOME 表示,
cd $GlassFish_HOME/glassfish4/glassfish/bin asadmin start-domain
更多的关于 GlassFish 4 的文档,请参考: http://glassfish.java.net/documentation.html
部署 OSGi 应用程序的方式
基本上,使用 GlassFish 4 部署 OSGi 应用程序有三种方式,
-
使用 asadmin deploy 命令
在命令行或者 Shell 中,使用类似如下的命令,
asadmin deploy –type=osgi XXX.jar 或 XXX.war
当部署 WAB 时,经常容易遗漏—type=osgi,如果遗漏这个选项,那么你所做的就是在部署一个标准的 WAR 而不是 WAB。
-
使用 autodeploy 的方式
这是一个非常快捷的部署方式,你只需要将要部署的 Bundle 放到 $GlassFish_HOME/glassfish4/glassfish/domains/domain1/autodeploy/bundles 目录下就可以了。这种方式是将 Apache Felix File Install[11] 集成到 GlassFish 中,使用这种方式甚至能够考虑 Bundle 之间的依赖。详细地内容,请看一下 [12]。
-
使用 asadmin osgi 命令
GlassFish 3 允许你通过 telnet 登陆到 GlassFish OSGi 运行时的后台,然后通过以下的方式来安装并启动一个 WAB,
install webbundle:file:///tmp/mybundle.war start <bundle_id>
但是,到了 GlassFish 4,这种 telnet 的方式已经被禁止了,原因是 telnet 的方式并不安全,因此,GlassFish 4 提供了一种新的方式去直接操作 OSGi 运行时,即通过执行 asadmin osgi …命令,例如,上面的命令等同于以下,
asadmin osgi install file:///tmp/mybundle.war asadmin osgi start <bundle_id>
对于 asadmin osgi 命令,最常用的就是,当你部署完一个 OSGi Bundle 或者想看一下某些 Bundle 的 Id 或者当前状态时,使用 asadmin osgi lb 命令能够列举出 OSGi 运行时中所有的 Bundle。
对于这三种方式,我更加倾向于使用“使用 autodeploy 的方式“,因为它更加简单,更有效率。对于“使用 asadmin deploy 命令”,绝大多数场合,执行的效率也很好,但是,当你的程序使用 vaadin 时,部署将会非常慢,这是 GlassFish 需要急需改进的一个特性,相信很快将会得到改善。
部署并运行 Sample 应用程序
现在,我们可以按照如下的顺序部署并运行 Sample 应用程序了,
- 部署 glassfish.wab.sample.core
执行“asadmin deploy –type=osgi glassfish.wab.sample.core.jar”
2. 部署 glassfish.wab.sample.web.war
执行“asadmin deploy –type=osgi glassfish.wab.sample.web.war“
3. 在浏览器上键入“ http://localhost:8080/wabsample/ “,应该没有出现任何模块,如下图所示,
4. 部署 glassfish.wab.sample.module1 和 glassfish.wab.sample.module2
执行“asadmin deploy –type=osgi glassfish.wab.sample.module1.war“ 以及”asadmin deploy –type=osgi glassfish.wab.sample.module2.war“
5. 在浏览器上点击刷新按钮,此时,模块 1 和模块 2 都出现了,如下图所示,
然后,再执行“asadmin osgi lb“命令看一下刚刚我们部署的 Bundle 的状态,
6. 执行以下命令卸载模块 2
“asadmin undeploy glassfish.wab.sample.module2“
7. 然后,在浏览器上再次点击刷新按钮,此时,模块 2 已经消失了,如下图所示,
剖析 GlassFish OSGi/WEB 容器
到这里为止,如果你仔细阅读上面的内容,我想你应该已经掌握了如何开发和部署一个 WAB,并且也应该理解了 WAB 和标准 OSGi Bundle 以及和标准 WAR 的区别。让我们再深入一下,看看 GlassFish 是如何实现 OSGi WEB 应用程序规范的。
混合应用程序 Bundle(Hybrid Application Bundle)
从 GlassFish 的角度看,WAB 又是混合应用程序 Bundle 的一种类型。混合应用程序 Bundle 既是一个标准 OSGi Bundle,又是一个 JavaEE 模块。在运行时,它既有一个 OSGi Bundle 上下文,又有一个 JavaEE 上下文。目前,GlassFish 支持两种类型的混合应用程序 Bundle,Web 应用程序 Bundle 和 EJB 应用程序 Bundle。关于 EJB 应用程序 Bundle,我将放在 Part3 中。
当一个混合应用程序 Bundle 被部署到 GlassFish OSGi 运行时,GlassFish 能够观察到它的生命周期,使用熟知的“Extender 模式 [13]“,将 Bundle 中的一些部分部署或 Undeploy 到 JavaEE 容器中。混合应用程序 Bundle 的生命周期如下所示,
图 6: 混合应用程序 Bundle 的生命周期
摘自: “OSGi Application Development using GlassFish Server“
如果你仔细看一下图 6 和图 1,本质上两幅图是一样的,图 6 并没有在 OSGi 生命周期的基本状态上增加 4 个部署和 Undeploy 相关的状态,但是,图 1 中的 4 个状态所涉及的操作都反映到了图 6 中。
GlassFish OSGi Web 容器的实现
GlassFish OSGi Web 容器实现了 OSGi Web 应用程序规范。通过部署 WAB,我们能够清晰地理解 GlassFish 部署 OSGi Web 应用程序的流程以及如何实现规范的。部署流程分为两个阶段,
- 和部署标准 OSGi Bundle 一样,部署 WAB 到 OSGi 运行时中。
- 当 WAB 的生命周期变为 ACTIVE 状态或者 STARTING 状态 (因为有一个懒惰的激活策略) 时,部署该 WAB 到 JavaEE 运行时中。
需要注意的是,1 和 2 是异步的,这与 Undeploy 过程不同,Undeploy 是同步的,也就是说,一旦该 WAB 被停止或卸载,将立即从 JavaEE 运行时中 Undeploy 该 WAB,并且清理相应的资源。
以下,我将使用“asadmin deploy 命令”来剖析部署的流程。
【阶段 1】部署 WAB 到 OSGi 运行时
阶段 1 的部署主要包括两个部分: a. 安装 WAB 到 OSGi 运行时 b. 启动该 WAB 使其处于 ACTIVE 状态或者 STARTING 状态。
以下是部署 WAB 到 OSGi 运行时的时序图,
图 7: 部署 WAB 到 OSGi 运行时的时序图
- 根据部署的类型, ApplicationLifecycle 类获取相应的 AchiveHandler。因为我们正在部署 WAB,当执行“asadmin deploy“命令时,我们传递了“—type=osgi”,因此,部署的类型为 osgi。获取到的 AchiveHandler 是 OSGiArchiveHandler 。AchiveHandler 负责处理和访问某种特定档案中的资源,这些档案包括了 WAR,JAR,RAR 以及 Bundle。AchiveHandler 将在构建部署 ClassLoader,获取 Sniffer 等后续动作中被使用到。
另外,ApplicationLifecycle 类是部署的核心类,也是部署命令核心逻辑执行的入口点,从它的所在的位置能够看出它的重要性,它位于 GlassFish 内核模块。
2. 接下来,ApplicationLifecycle 类通过 SnifferManagerImpl 类获取相应的 Sniffer。那么,什么是 Sniffer 呢? 自从 GlassFish v3 开始,根据部署的请求,Sniffer 被用来分析和选择合适的容器来处理应用程序的类型。分析和选择的过程可能简单,也可能复杂。例如,通过查找 WEB-INF/web.xml 或者是否以.war 结尾来分析是否需要 WEB 容器,也可能通过注解 (Annotation) 扫描来判断是否需要 EJB 容器。对于 WAB 的情形,SnifferManagerImpl 返回了 OSGiSniffer 。进一步地,Sniffer 接口有个重要的方法叫“getContainersNames”,对于 OSGiSniffer,这个方法返回“osgi”。这个方法将被用来获取相应的容器。
3. 有了具体的 Sniffer 之后,ApplicationLifecycle 类通过 ContainerRegistry 类的 getContainer(String containerType) 方法来获取相应的容器,其中,containerType 就是 2) 中提到的“getContainersNames”的返回值。进一步地,getContainer(String containerType) 方法返回了一个 EngineInfo 对象,这个对象拥有特定容器的信息。对于 WAB 情形,这个特定的容器是 OSGiContainer 。以下是一个调试的信息,给出了 EngineInfo 对象中的内容。
4. 其中,你可以发现 container 的类型是一个 ServiceHandleImp,这是一个 HK2 相关的类,以下是 OSGiContainer 的代码,
@Service(name = OSGiSniffer.CONTAINER_NAME) @Singleton public class OSGiContainer implements Container { public Class<? extends Deployer> getDeployer() { return OSGiDeployer.class; } public String getName() { return OSGiSniffer.CONTAINER_NAME; // used for reporting purpose,so any string is fine actually } }
关于 HK2 的内容,我将在 Part7 中详细阐述。这里简单地说一下,首先,HK2 是一个 JSR330 的实现。其次,OSGiContainer 使用 @Service 来标注这个类是一个 HK2 的服务,并且用 name 属性来方便 HK2 进行依赖注入。另外,使用 @Singleton 来标注当使用 HK2 获取 OSGiContainer 实例时,使用 Singleton 的方式。再者,这个类中最为重要的方法是 getDeployer,该方法返回了 OSGiDeployer ,用于后续的 OSGi 部署。
从以上的定义能够看出,OSGiContainer 的实例由 HK2 负责创建并不是通过 new 出来的,因此,EngineInfo 对象中的内容很自然地变成了 ServiceHandleImp。
5. 接下来就是通过 EngineInfo 对象获取相应的 Deployer 了,Deployer 真正负责部署{3) 中我们已经知道对于 WAB 情形,EngineInfo 将返回 OSGiDeployer。
6. 然后,ApplicationLifecycle 类委托 OSGiDeployer 类来安装 WAB 到 OSGi 运行时中,OSGiDeployer 进而使用 BundleContext 来安装该 WAB。安装成功后,该 WAB 的状态将变为 INSTALLED。
7. 当安装成功后,ApplicationLifecycle 类开始委托 OSGiDeployedBundle 类来启动该 WAB,当然,在启动之前,需要首先判断该 Bundle 不是一个 Fragment,然后再通过 Bundle.start 方法来启动该 WAB。
上面提到的 Sniffer 等概念,在 GlassFish Wiki[14] 中有更为详细地说明。
【阶段 2】部署 WAB 到 JavaEE 运行时
在阐述阶段 2 之前,需要先回到 GlassFish Domain 的启动,这部分内容将在 Part8 中详细地说明。也许你会问,为什么要回到 GlassFish Domain 的启动?
原因在于从阶段 1 过渡到阶段 2,需要做一些必要的工作,例如: 在“WAB 生命周期”一章中,提到过为了部署 WAB 到 JavaEE 运行时,前提条件是等待 WAB 处于 ACTIVE 状态或 STARTING 状态,那么如何等待?在 OSGi 开发中,一个常见的模式是使用 BundleTracker 类来跟踪已被安装的 Bundle 的生命周期变化。通常,打开 BundleTracker 的操作是由 OSGi Activator 完成的,而 OSGi Activator(如果有的话) 是启动 OSGi Bundle 最先执行的方法,因此,必须有一个 Bundle 做这样的 BootStrap 动作。GlassFish OSGi-JavaEE 遵循了这一设计模式,所以,为了搞清楚哪些 Bundle 在完成这些 BootStrap 动作,我们必须回到 GlassFish Domain 的启动。
GlassFish 安装目录下有个目录叫 glassfish4/glassfish/modules/autostart,这里放置了一些 Bundle,其中,有两个 Bundle 与本文密切相关: 1) osgi-javaee-base.jar 2) osgi-web-container.jar。
首先,看一下它们的作用,osgi-javaee-base 是 GlassFish OSGi-JavaEE 实现的基类,主要使用了 Extender 模式来构建整个 OSGi-JavaEE 的框架,是 GlassFish OSGi-JavaEE 实现的灵魂。osgi-web-container 实现了 OSGi Web 规范,也是本文重点要剖析的对象。
其次,osgi-javaee-base 和 osgi-web-container 都定义了 Activator,当启动 GlassFish Domain 后,osgi-javaee-base.jar 和 osgi-web-container.jar 被部署到 GlassFish OSGi 运行时中,且这两个 Bundle 都被激活处于 Active 状态,在到达 Active 状态之前,各自的 Activator 都被调用。让我们来看看它们的 Activator 都做了什么。
- osgi-javaee-base 的 Activator
osgi-javaee-base 的 Activator 叫“OSGiJavaEEActivator”,它的 start 方法中核心的逻辑是启动 ExtenderManager,以及注册并启动 JavaEEExtender。ExtenderManager 的作用是负责启动任何已经被注册的 Extender 服务。以下是相应的代码,
private synchronized void startExtenders() { //Because of a race condition,we can be started multiple times, so check if already started if (extenderTracker != null) return; // open will call addingService for each existing extender // and there by we will start each extender. extenderTracker = new ExtenderTracker(context); extenderTracker.open(); }
可以清楚地看到,启动的逻辑主要在 ExtenderTracker 中,让我们看一下
private class ExtenderTracker extends ServiceTracker { ExtenderTracker(BundleContext context) { super(context, Extender.class.getName(), null); } @Override public Object addingService(ServiceReference reference) { Extender e = Extender.class.cast(context.getService (reference)); logger.logp(Level.FINE, "ExtenderManager$ExtenderTracker"," addingService", "Starting extender called {0}", new Object[]{e}); e.start(); return e; } …
ExtenderTracker 是一个 ServiceTracker,在 OSGi 开发中,使用 ServiceTracker 来跟踪注册的 OSGi 服务已经成为了经典的模式。这里,ExtenderTracker 跟踪的服务类型是 Extender 接口。一旦某个 Extender 被注册,那么 ExtenderTracker 将调用 addingService 方法然后启动这个 Extender。
前面提到,除了启动 ExtenderManager,osgi-javaee-base 也注册并启动 JavaEEExtender,这个 JavaEEExtender 非常重要,它的作用就是负责侦听和部署混合应用程序 Bundle。看一下它的 start 方法,
public synchronized void start() { executorService = Executors.newSingleThreadExecutor(); c = new OSGiContainer(context); c.init(); reg = context.registerService(OSGiContainer.class.getName(), c, null); tracker = new BundleTracker(context, Bundle.ACTIVE | Bundle. STARTING, new HybridBundleTrackerCustomizer()); tracker.open(); }
其中,最重要的是初期化并注册 OSGiContainer 以及打开一个 BundleTracker 来跟踪混合应用程序 Bundle 是否处于 Active 或 Starting 状态。对于 OSGiContainer,它具体负责了部署的过程,搭建了部署的骨架。对于 BundleTracker 来说,它回答了早期提到的“如何等待 WAB 处于 ACTIVE 状态或 STARTING 状态”的问题。对于 HybridBundleTrackerCustomizer 类,其中的 addingBundle 方法值得我们看一下,
public Object addingBundle(final Bundle bundle, BundleEvent event) { if (!isStarted()) return null; final int state = bundle.getState(); if (isReady(event, state)) { Future<OSGiApplicationInfo> future = executorService. submit(new Callable<OSGiApplicationInfo>() { @Override public OSGiApplicationInfo call()throws Exception{ return deploy(bundle); } }); deploymentTasks.put(bundle.getBundleId(), future); return bundle; } return null; }
可以清晰地看到,一旦混合应用程序 Bundle 处于 Active 或 Starting 状态,那么,立刻启动一个线程进行部署。
2. osgi-web-container 的 Activator
osgi-web-container 的 Activator 是 OSGiWebContainerActivator,这个类的 start 方法很简单,注册 WebExtender 作为 OSGi 服务。可以看出,osgi-web-container 遵循了 Extender 模式,一旦注册成功,osgi-javaee-base 中的 ExtenderTracker 将跟踪到它并调用它的 start 方法。下图是 WebExtender 的主要处理逻辑,
阶段 2 的前传已经讲完,接下来,回到阶段 2 的部署上来,以下是阶段 2 中主要的部署时序图,
图 9: 阶段 2 中主要的部署时序图
下面,详细地说明一下图 9 中的各个时序动作,
- 当 JavaEEExtender 中的 HybridBundleTrackerCustomizer 跟踪到 WAB 处于 Active 或 Starting 状态时,开始调用 OSGiContainer 的 deploy 方法,这里的 OSGiContainer 来自 osgi-javaee-base 模块并不是阶段 1 中提到的 OSGiContainer,请注意区分。
- OSGiContainer 的 deploy 方法首先选择正确的 Deployer,方法是通过遍历所有已经注册的 OSGiDeployer 服务,然后逐个调用这些 OSGiDeployer 服务的 handles 方法来选择正确的 Deployer。对于 WAB 情形,正确的 Deployer 是 OSGiWebDeployer,它的 handles 方法如下:
final Dictionary headers = b.getHeaders(); return headers.get(Constants.WEB_CONTEXT_PATH) != null && headers.get(org.osgi.framework.Constants.FRAGMENT_HOST) == null;
很清晰地看到,如果当前 Bundle 的元数据中包含了 Web-ContextPath 且不包含 Fragment-Host,那么该 Bundle 是一个 WAB,且 OSGiWebDeployer 能够处理这种类型的混合应用程序 Bundle。
3. 选择完正确的 Deployer 后,OSGiContainer 委托 OSGiWebDeployer 执行具体的部署。OSGiWebDeployer 的 deploy 方法首先创建 OSGi 部署请求 (OSGiWebDeploymentRequest),然后调用 OSGiWebDeploymentRequest 的 execute 方法进行部署,在 execute 方法中,主要执行预处理 (preDeploy),部署的准备工作 (prepare),实际的部署 (deploy),以及后处理 (postDeploy)。
4. 预处理的核心逻辑是使用 ContextPathCollisionDetector 检测 Web 上下文路径冲突,这是 OSGi Web 规范必须的。
5. 部署的准备工作中最重要的是创建一个 OSGiWebDeploymentContext,OSGiWebDeploymentContext 是 GlassFish WAB 支持的心脏,它负责为 WAB 创建一个类加载器 (class loader) 以便当有 Web 请求时,Web 容器能够正确地加载到 WAB 中相关的静态资源和动态资源。这个类加载器为 WABClassLoader,这个类加载器继承了 org.glassfish.web.loader.WebappClassLoader,而后者专门是 GlassFish Web 容器用来实现资源加载的。为了创建这个类加载器,需要重载 OSGiDeploymentContext.setupClassLoader 方法,如下所示:
protected void setupClassLoader() throws Exception { finalClassLoader = new WABClassLoader(null); shareableTempClassLoader = finalClassLoader; WebappClassLoader.class.cast(finalClassLoader).start(); }
- 准备工作做完后,开始执行实际的部署,你可能已经发现,实际的部署再次委托到了阶段 1 中提到的 ApplicationLifecycle,是的,我们不需要再次发明轮子,因为 ApplicationLifecycle 在那里,通过它,将这个 WAB 展开到 glassfish4/glassfish/domains/domain1/applications 中,在 domain.xml 中写入部署的信息等,总之,像部署标准 WAR 一样去部署它。
- 最后,还是要执行一下后处理工作,因为,一旦前面的动作部署失败了,总是需要进行回滚使系统的状态恢复到之前。
至此,WAB 的部署以及相关的实现逻辑已经写完了,详细的代码可以使用 SVN 下载 GlassFish FighterFish 子工程 ( https://svn.java.net/svn/glassfish~svn/trunk/fighterfish ) 来研究一下。
最后,想简单地说一下对于未来的一些思考。
思考
制作新的 Maven Archetype
在 Part1 写完后,我看到有朋友在评论部分提到了 OSGi 中看不中用,从本文的 WAB 实例的构建看,确实也有不便之处。对于一些人为需要手动进行配置的部分 (如 pom 文件),最好能够尽可能的自动化。这项工作已经开始了!我和我的同事程晓明 (@程 _ 晓明) 以及 GlassFish FighterFish 子工程的 leader(Sahoo) 正在制作新的 Maven Archetype 以便自动化一些繁琐的构建工作,应该很快就会面世。
关于 Sample 的实用性
本文的 Sample 只是作为演示用,距离真正的实用性还有不小的差距,尤其是需要解决一个 Bundle 之间共享 CDI BeanManager 的问题,例如,让模块 1 中也能够使用 JSF 托管的 Bean,然后这并不是一件容易的事情,这需要在 Web 模块和模块 1 中架起一座桥梁,以便 Web 模块中的 BeanManager 能够发现模块 1 中的 CDI Bean。这个问题目前正在和 JSF 以及 CDI 的 leader 进行讨论,期待能够尽快解决。
参考
- ”OSGi Enterprise Release 5 Specification”
- “Java™ Servlet Specification Version 3.1, 3.5 Request Path Elements”
- “CDI Events don’t work when fired by a OSGi ServiceListener”
- “Maven Multi-Module Quickstart”
- “Bundle Plugin for Maven“
- “Apache Maven WAR Plugin“
- “Maven Compiler Plugin“
- “Bnd“
- “Bnd 06 Instructions “
- “Maven M2_REPO is non modifiable“
- “Apache Felix File Install“
- “Using filesystem operations to manage OSGi bundles in GlassFish“
- “Extender 模式“
- “Container SPI for GlassFish v3“
作者简介
汤泳,高级工程师,硕士,2004 年毕业于南京理工大学计算机科学与技术系。现就职于南京富士通南大软件技术有限公司。南京 Java User Group 的负责人之一。 2013 年 2 月成为 GlassFish OSGi 以及 OSGi-JavaEE 模块的 Committer, 同时, 他也是 OSGi Alliance 的 Supporter 和 OSGi China Forum 的核心成员。除了长期贡献 GlassFish, 他也积极活跃在多个 Apache 开源社区,如 Apache JClouds, Apache Karaf 以及 Apache Aries。
他的 E-Mail: tangyong@cn.fujitsu.com 或者 tangyong.gf@gmail.com
LinkedIn : http://www.linkedin.com/pub/tang-yong/21/62b/809
Blog: http://osgizone.typepad.com/
新浪 WeiBo: @widefish
评论