在Part1中,我们提到了企业级OSGi制定了一系列的规范来与JavaEE集成,其中,最具代表性的规范是OSGi WEB应用程序规范,这部分将带领大家深入理解OSGi WEB应用程序规范和GlassFish OSGi/WEB容器。本文将分成以下几个部分:
在信息和网络发达的今天,WEB应用程序已经非常得流行和普遍,一些任务关键型(Mission-Critical)的WEB应用程序每天都在高负荷地运行,很少有中断,因为一次不经意的中断可能造成数据的大规模丢失,以至损失大量的资金而造成严重的后果。这些任务关键型的WEB应用往往出现在证券和股票等相关的金融行业。现在,我们开始考虑一个场景: 几个星期或者几个月甚至几年后,WEB应用的客户或者提供商希望在WEB前端增加一些新的模块或功能,但是,为了增加这些新的模块,我们不能停止WEB应用,而且也不希望再次重构或者改变WEB应用的现有架构和模块。这听起来不可思议,对于这样一个场景,至少应该停止应用程服务的实例吧。但是,客户不会答应。另一方面,在当今大数据的时代,每一秒钟都会有大量的数据进入我们的应用之中。那么,如何解决这样的场景?
一个可行的答案是: 使用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的区别。
在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本身就是一个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。
上面已经看到,除了标准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所示的那样,
通过对MANIFEST.MF追加OSGi元数据,也再次说明了WAB使用OSGi生命周期和类/资源加载规则而不是标准JavaEE环境的加载规则,这点至关重要。
在图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运行时。具体地,
如果3的验证通过,那么按照以下的顺序,Web运行时开始处理部署相关的细节,如果web.xml存在的话,它也会处理web.xml中的内容。
如果在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的主要步骤如下:
方法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容器,在上面的章节中我们已经多次提到了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应用程序运行时环境,对于GlassFish来说,Web运行时基于Tomcat Catalina。
一个URL Stream Handler,这个URL Stream Handler能够处理webbundle: scheme,这个scheme能够被用来转换以及安装WAR到OSGi运行时中,GlassFish 4提供了一个新的特性,即通过实现这个URL Stream Handler在部署时自动转换和安装WAR到OSGi运行时。
回到开始提出的问题场景,即如何在不停止JVM的情况下,构建一个动态的Web应用程序?
以下的Sample应用程序源于我曾经调查的一个GlassFish问题 [3],先看一下需求,
我们希望构建这样一个Web应用程序,当启动这个Web应用程序时,没有任何Web模块被加载,界面显示“No modules available.”,然后,当部署一个Web模块后,在浏览器上点击刷新按钮(不重启应用程序),这个Web模块随即出现在界面上。
在本文中我将使用如下的一些工具来构筑开发环境,其中,根据个人的使用习惯,你也可能使用NetBeans或IntelliJ IDEA。
下面详细地说明一下图4,
我们将使用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文件。
配置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的配置中,还出现了<Export-Package>!*</Export-Package>,这个标签以及标签值的含义是,默认地,这个OSGi Bundle不导出任何包,除非我们显示地在osgi.properties中指定“Export-Package”值。
创建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将导出内部的带有工程版本的包。
创建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,你也可以自行修改为其他的值。
创建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,这里就跳过。
设置开发环境
一旦这些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运行时的服务注册表中。
详细的内容请参见: https://github.com/tangyong/GlassFishOSGiJavaEESample/tree/master
/glassfish.wab.sample/glassfish.wab.sample.module1
glassfish.wab.sample.module2
类似于模块1,这里就省略跳过。
完整的Sample应用程序,请从https://github.com/tangyong/GlassFishOSGiJavaEESample 中下载。
一旦我们构建完Sample应用程序,就将使用GlassFish 4来部署它。
首先,你需要从以下链接下载一个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
基本上,使用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应用程序了,
部署glassfish.wab.sample.core
执行“asadmin deploy –type=osgi glassfish.wab.sample.core.jar”
部署glassfish.wab.sample.web.war
执行“asadmin deploy –type=osgi glassfish.wab.sample.web.war“
在浏览器上键入“http://localhost:8080/wabsample/“,应该没有出现任何模块,如下图所示,
部署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“
在浏览器上点击刷新按钮,此时,模块1和模块2都出现了,如下图所示,
然后,再执行“asadmin osgi lb“命令看一下刚刚我们部署的Bundle的状态,
执行以下命令卸载模块2
“asadmin undeploy glassfish.wab.sample.module2“
然后,在浏览器上再次点击刷新按钮,此时,模块2已经消失了,如下图所示,
到这里为止,如果你仔细阅读上面的内容,我想你应该已经掌握了如何开发和部署一个WAB,并且也应该理解了WAB和标准OSGi Bundle以及和标准WAR的区别。让我们再深入一下,看看GlassFish是如何实现OSGi WEB应用程序规范的。
从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容器实现了OSGi Web应用程序规范。通过部署WAB,我们能够清晰地理解GlassFish部署OSGi Web应用程序的流程以及如何实现规范的。部署流程分为两个阶段,
需要注意的是,1和2是异步的,这与Undeploy过程不同,Undeploy是同步的,也就是说,一旦该WAB被停止或卸载,将立即从JavaEE运行时中Undeploy该WAB,并且清理相应的资源。
以下,我将使用“asadmin deploy命令”来剖析部署的流程。
阶段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内核模块。
接下来,ApplicationLifecycle类通过SnifferManagerImpl类获取相应的Sniffer。那么,什么是Sniffer呢?自从GlassFish v3开始,根据部署的请求,Sniffer被用来分析和选择合适的容器来处理应用程序的类型。分析和选择的过程可能简单,也可能复杂。例如,通过查找WEB-INF/web.xml或者是否以.war结尾来分析是否需要WEB容器,也可能通过注解(Annotation)扫描来判断是否需要EJB容器。对于WAB的情形,SnifferManagerImpl返回了OSGiSniffer。进一步地,Sniffer接口有个重要的方法叫“getContainersNames”,对于OSGiSniffer,这个方法返回“osgi”。这个方法将被用来获取相应的容器。
有了具体的Sniffer之后,ApplicationLifecycle类通过ContainerRegistry类的getContainer(String containerType)方法来获取相应的容器,其中,containerType就是2)中提到的“getContainersNames”的返回值。进一步地,getContainer(String containerType)方法返回了一个EngineInfo对象,这个对象拥有特定容器的信息。对于WAB情形,这个特定的容器是OSGiContainer。以下是一个调试的信息,给出了EngineInfo对象中的内容。
其中,你可以发现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。
接下来就是通过EngineInfo对象获取相应的Deployer了,Deployer真正负责部署{3)中我们已经知道对于WAB情形,EngineInfo将返回OSGiDeployer。
然后,ApplicationLifecycle类委托OSGiDeployer类来安装WAB到OSGi运行时中,OSGiDeployer进而使用BundleContext来安装该WAB。安装成功后,该WAB的状态将变为INSTALLED。
当安装成功后,ApplicationLifecycle类开始委托OSGiDeployedBundle类来启动该WAB,当然,在启动之前,需要首先判断该Bundle不是一个Fragment,然后再通过Bundle.start方法来启动该WAB。
上面提到的Sniffer等概念,在GlassFish Wiki[14]中有更为详细地说明。
在阐述阶段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状态,那么,立刻启动一个线程进行部署。
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中的各个时序动作,
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。
部署的准备工作中最重要的是创建一个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(); }
至此,WAB的部署以及相关的实现逻辑已经写完了,详细的代码可以使用SVN下载GlassFish FighterFish子工程(https://svn.java.net/svn/glassfish~svn/trunk/fighterfish)来研究一下。
最后,想简单地说一下对于未来的一些思考。
在Part1写完后,我看到有朋友在评论部分提到了OSGi中看不中用,从本文的WAB实例的构建看,确实也有不便之处。对于一些人为需要手动进行配置的部分(如pom文件),最好能够尽可能的自动化。这项工作已经开始了!我和我的同事程晓明(@程_晓明)以及GlassFish FighterFish子工程的leader(Sahoo)正在制作新的Maven Archetype以便自动化一些繁琐的构建工作,应该很快就会面世。
本文的Sample只是作为演示用,距离真正的实用性还有不小的差距,尤其是需要解决一个Bundle之间共享CDI BeanManager的问题,例如,让模块1中也能够使用JSF托管的Bean,然后这并不是一件容易的事情,这需要在Web模块和模块1中架起一座桥梁,以便Web模块中的BeanManager能够发现模块1中的CDI Bean。这个问题目前正在和JSF以及CDI的leader进行讨论,期待能够尽快解决。
汤泳,高级工程师,硕士,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: [email protected] 或者[email protected]
LinkedIn : http://www.linkedin.com/pub/tang-yong/21/62b/809
Blog: http://osgizone.typepad.com/
新浪WeiBo: @widefish