打造一个基于OSGi的Web Application

动机和目标

OSGi技术发展至今也有好几年了,然而除了在富客户端应用(以Eclipse为代表)和服务器应用(如大多数的应用服务器)方面大放光芒之外,在 Web Application方面的应用和资料却少之又少。一方面,在OSGi规范中,对于Web应用方面的规划尚不成熟,即使在最新的4.2版中,也仅仅只有 一个HttpService,这个简陋的service甚至不能覆盖任何一个现有的Servlet规范;另一方面,各个OSGi实现厂商对 HttpService的实现也是不完全的,在开发实现一个常规的Web Application时,这些实现也是完全不够用的。本文章的目的,也就是为了探索OSGi在Web Application上的开发之路该如何走,从我的视角提出一些看法,做一些尝试,希望对大家有所帮助。

现在OSGi与Web Application的结合,大致有两个方向:

  1. OSGi包含Web Container:目前能完美嵌入OSGi的Web Container似乎只有jetty一个,tomcat的catalina似乎有希望能成为第二个。我们完全不能指望Websphere和 Weblogic能在短期内具有能嵌入OSGi的能力,所以这个方向理所当然的被我放弃了。
  2. Web Container包含OSGi:这个方面目前只有equinox的Servlet Bridge这么一个著名的实现,equinox通过Servlet Bridge的方式来实现一个OSGi的HttpService服务,这个服务目前能做的事情还非常有限,还不足以覆盖Servlet规范。

我的目标是构建一个OSGi与Web Application结合的方式,它要能满足一下几点需求:

  1. 基于OSGi的bundle和service。
  2. 适合绝大对数支持Servlet 2.4和Jsp 2.0规范的Web服务器。
  3. 适合现有的实现OSGi 4.2规范的OSGi Framework实现:equinox、felix和knopflerfish。
  4. 支持大部分Servlet 2.4和Jsp 2.0规范中声明的功能。
  5. 提供一个基于HttpService的服务实现,以此来兼容其他使用HttpService的service。

毫无疑问,我将采用Web Container中包含OSGi的方式来实现,具体的内容将在以后陆续提供。

搭建开发环境

工欲善其事必先利其器,在正式开发之前,花一点时间来构建开发环境还是有必要的。本章介绍一下我的开发环境。

我使用的开发环境如下:

  1. Eclipse:当然了,最新版3.52,其中包含了最新版的WTP(Eclipse Web Tools Platform),个人感觉,不比MyEclipse差,而且最重要的是,它是free的。
  2. equinox-SDK:版本为3.6M5,实现了OSGi R4 core framework specification 4.2。
  3. Tomcat:作为第一个实现的Web Container,我采用了Tomcat,从中抽取几个特定版本作为测试对象:5.5.28和6.0.26这两个版本,因为他们支持Java5和 Servlet2.4/Jsp2.0。
  4. JDK:当然Java5以上的,谁叫Equinox只支持Java5以上的呢,我采用的是jdk1.5.0.22。基于Websphere和 Weblogic的缓慢的JDK升级历程,我还是决定不采用Java6或者是7了。

以下是我的目录结构:
打造一个基于OSGi的Web Application
环境整合:
1.运行Eclipse,指定Workspace路径为:D:\dbstar\workspaces\OSGi
2.设置Plug-in Development的Target Platform,增加equinox-SDK-3.6M5并设为默认,这样我们就可以使用equinox-SDK-3.6M5来作为我们开发 bundle的基准库,而不是使用Eclipse自带的plugin开发环境。
打造一个基于OSGi的Web Application
3.在Server配置中增加Tomcat两个版本的服务器。
打造一个基于OSGi的Web Application

自此,我的开发环境就已经设置好了,当然了,还有一些其他的个人习惯设置,比如说字体,默认编码设为UTF-8,Code Template和Formatter等等,就不一一赘述了。

在下面一篇中,将介绍如何在Web Application中启动OSGi。

在WebApplication中启动 OSGi

本章将创建一个Web Application项目,并描述如何在此应用中启动OSGi。

首先,在Eclipse中创建一个Dynamic Web Project,名字为OSGi-Web,Context root为osgi。
打造一个基于OSGi的Web Application
这个项目只作为部署Web Application使用,相关java代码放在另外一个Java Project中,因此我们再创建一个新的Java Project,名字为OSGi-Web-Launcher。然后在OSGi-Web项目的Java EE Module Dependencies中设置OSGi-Web-Launcher为关联,这样在部署的时候,OSGi-Web-Launcher项目中的java代码 将为打包为jar存放到Web的WEB-INF/lib目录之中。

为了启动OSGi,我们在web中增加一个ServletContextListener监听器实现,并且通过这个监听器来控制OSGi容器的启动和终 止。

在OSGi-Web-Launcher项目中增加一个java类,类名为FrameworkConfigListener,实现接口 ServletContextListener,package为org.dbstar.osgi.web.launcher。在 contextInitialized方法中,增加启动OSGi的代码,在contextDestroyed方法中,增加停止OSGi的代码,这样我们就 可以使OSGi容器的生命周期与ServletContext的生命周期保持一致了。

启动OSGi容器:
感谢OSGi规范4.2给了我们一个简单统一的启动OSGi容器的方式,所有实现OSGi4.2规范的容器实力都应该实现这种启动方式,那就是通过 org.osgi.framework.launch.FrameworkFactory,同时,还必须在其实现jar中放置一个文件:META- INF/services/org.osgi.framework.launch.FrameworkFactory,这个文件中设置了实际的 FrameworkFactory实现类的类名。在equinox-SDK-3.6M5的 org.eclipse.osgi_3.6.0.v20100128-1430.jar中,这个文件的内容 是:org.eclipse.osgi.launch.EquinoxFactory。

我们先写一个工具类来载入这个配置文件中的内容:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 package org.dbstar.osgi.web.launcher;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.InputStreamReader;
7
8 public abstract class ServiceLoader{
9 public final static < E > Class < E > load(Class < E > clazz) throws IOException,ClassNotFoundException{
10 return load(clazz,Thread.currentThread().getContextClassLoader());
11 }
12
13 @SuppressWarnings( " unchecked " )
14 public final static < E > Class < E > load(Class < E > clazz,ClassLoaderclassLoader) throws IOException,
15 ClassNotFoundException{
16 Stringresource = " META-INF/services/ " + clazz.getName();
17 InputStreamin = classLoader.getResourceAsStream(resource);
18 if (in == null ) return null ;
19
20 try {
21 BufferedReaderreader = new BufferedReader( new InputStreamReader(in));
22 StringserviceClassName = reader.readLine();
23 return (Class < E > )classLoader.loadClass(serviceClassName);
24 } finally {
25 in.close();
26 }
27 }
28 }


然后获取到FrameworkFactory的实例类:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 try {
2 frameworkFactoryClass = ServiceLoader.load(FrameworkFactory. class );
3 } catch (Exceptione){
4 throw new IllegalArgumentException( " FrameworkFactoryserviceloaderror. " ,e);
5 }
6 if (frameworkFactoryClass == null ){
7 throw new IllegalArgumentException( " FrameworkFactoryservicenotfound. " );
8 }


实例化FrameworkFactory:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 FrameworkFactoryframeworkFactory;
2 try {
3 frameworkFactory = frameworkFactoryClass.newInstance();
4 } catch (Exceptione){
5 throw new IllegalArgumentException( " FrameworkFactoryinstantiationerror. " ,e);
6 }


获取Framework的启动配置:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 Map < Object,Object > configuration;
2 try {
3 // 载入Framework启动配置
4 configuration = loadFrameworkConfig(event.getServletContext());
5 if (logger.isInfoEnabled()){
6 logger.info( " LoadFrameworkconfiguration:[ " );
7 for (Objectkey:configuration.keySet()){
8 logger.info( " \t " + key + " = " + configuration.get(key));
9 }
10 logger.info( " ] " );
11 }
12 } catch (Exceptione){
13 throw new IllegalArgumentException( " LoadFrameworkconfigurationerror. " ,e);
14 }


启动配置读取外部配置文件,可以在此配置文件中增加OSGi容器实现类相关的配置项,例如Equinox的osgi.console:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 // 载入Framework启动配置
2 private static Map < Object,Object > loadFrameworkConfig(ServletContextcontext) throws MalformedURLException{
3 StringconfigLocation = context.getInitParameter(CONTEXT_PARAM_OSGI_CONFIG_LOCATION);
4 if (configLocation == null )configLocation = DEFAULT_OSGI_CONFIG_LOCATION;
5 else if ( ! configLocation.startsWith( " / " ))configLocation = " / " .concat(configLocation);
6
7 Propertiesconfig = new Properties();
8 try {
9 // 载入配置项
10 config.load(context.getResourceAsStream(configLocation));
11 if (logger.isInfoEnabled())logger.info( " LoadFrameworkconfigurationfrom: " + configLocation);
12 } catch (IOExceptione){
13 if (logger.isWarnEnabled())logger.warn( " LoadFrameworkconfigurationerrorfrom: " + configLocation,e);
14 }
15
16 StringstorageDirectory = config.getProperty(PROPERTY_FRAMEWORK_STORAGE,DEFAULT_OSGI_STORAGE_DIRECTORY);
17 // 检查storageDirectory合法性
18 if (storageDirectory.startsWith(WEB_ROOT)){
19 // 如果以WEB_ROOT常量字符串开头,那么相对于WEB_ROOT来定 位
20 storageDirectory = storageDirectory.substring(WEB_ROOT.length());
21 storageDirectory = context.getRealPath(storageDirectory);
22 } else {
23 // 如果是相对路径,那么相对于WEB_ROOT来定位
24 if ( ! new File(storageDirectory).isAbsolute()){
25 storageDirectory = context.getRealPath(storageDirectory);
26 }
27 }
28 storageDirectory = new File(storageDirectory).toURL().toExternalForm();
29 config.setProperty(PROPERTY_FRAMEWORK_STORAGE,storageDirectory);
30 if (logger.isInfoEnabled())logger.info( " UseFrameworkStorage: " + storageDirectory);
31
32 return config;
33 }


然后,就可以获取framework实例了,通过framework来初始化,启动和停止OSGi容器:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 try {
2 framework = frameworkFactory.newFramework(configuration);
3 framework.init();
4
5 // 初始化Framework环境
6 initFramework(framework,event);
7
8 // 启动Framework
9 framework.start();
10
11 succeed = true ;
12 } catch (BundleExceptione){
13 throw new OSGiStartException( " StartOSGiFrameworkerror! " ,e);
14 } catch (IOExceptione){
15 throw new OSGiStartException( " InitOSGiFrameworkerror " ,e);
16 }


在initFramework方法中,主要做两件事情,一是将当前的ServletContext作为一个service注册到OSGi容器中去:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 private static void registerContext(BundleContextbundleContext,ServletContextservletContext){
2 Propertiesproperties = new Properties();
3 properties.setProperty( " ServerInfo " ,servletContext.getServerInfo());
4 properties.setProperty( " ServletContextName " ,servletContext.getServletContextName());
5 properties.setProperty( " MajorVersion " ,String.valueOf(servletContext.getMajorVersion()));
6 properties.setProperty( " MinorVersion " ,String.valueOf(servletContext.getMinorVersion()));
7 bundleContext.registerService(ServletContext. class .getName(),servletContext,properties);
8 }

第二件事就是:在第一次初始化容器时,加载并启动指定目录中的bundle:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 // 初始化Framework环境
2 private static void initFramework(Frameworkframework,ServletContextEventevent) throws IOException{
3 BundleContextbundleContext = framework.getBundleContext();
4 ServletContextservletContext = event.getServletContext();
5
6 // 将ServletContext注册为服务
7 registerContext(bundleContext,servletContext);
8
9 Filefile = bundleContext.getDataFile( " .init " );
10 if ( ! file.isFile()){ // 第一次初始化
11 if (logger.isInfoEnabled())logger.info( " InitFramework打造一个基于OSGi的Web Application " );
12
13 StringpluginLocation = servletContext.getInitParameter(CONTEXT_PARAM_OSGI_PLUGINS_LOCATION);
14 if (pluginLocation == null )pluginLocation = DEFAULT_OSGI_PLUGINS_LOCATION;
15 else if ( ! pluginLocation.startsWith( " / " ))pluginLocation = " / " .concat(pluginLocation);
16
17 // 安装bundle
18 FilebundleRoot = new File(servletContext.getRealPath(pluginLocation));
19 if (bundleRoot.isDirectory()){
20 if (logger.isInfoEnabled())logger.info( " LoadFrameworkbundlesfrom: " + pluginLocation);
21
22 FilebundleFiles[] = bundleRoot.listFiles( new FilenameFilter(){
23 public boolean accept(Filedir,Stringname){
24 return name.endsWith( " .jar " );
25 }
26 });
27
28 if (bundleFiles != null && bundleFiles.length > 0 ){
29 for (FilebundleFile:bundleFiles){
30 try {
31 bundleContext.installBundle(bundleFile.toURL().toExternalForm());
32 if (logger.isInfoEnabled())logger.info( " Installbundlesuccess: " + bundleFile.getName());
33 } catch (Throwablee){
34 if (logger.isWarnEnabled())logger.warn( " Installbundleerror: " + bundleFile,e);
35 }
36 }
37 }
38
39 for (Bundlebundle:bundleContext.getBundles()){
40 if (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED){
41 if (bundle.getHeaders().get(Constants.BUNDLE_ACTIVATOR) != null ){
42 try {
43 bundle.start(Bundle.START_ACTIVATION_POLICY);
44 if (logger.isInfoEnabled())logger.info( " Startbundle: " + bundle);
45 } catch (Throwablee){
46 if (logger.isWarnEnabled())logger.warn( " Startbundleerror: " + bundle,e);
47 }
48 }
49 }
50 }
51 }
52
53 new FileWriter(file).close();
54 if (logger.isInfoEnabled())logger.info( " Frameworkinited. " );
55 }
56 }


以上就是启动OSGi容器的过程,相比较而言,停止容器就简单多了:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 public void contextDestroyed(ServletContextEventevent){
2 if (framework != null ){
3 if (logger.isInfoEnabled())logger.info( " StoppingOSGiFramework打造一个基于OSGi的Web Application " );
4
5 boolean succeed = false ;
6 try {
7 if (framework.getState() == Framework.ACTIVE)framework.stop();
8 framework.waitForStop( 0 );
9 framework = null ;
10
11 succeed = true ;
12 } catch (BundleExceptione){
13 throw new OSGiStopException( " StopOSGiFrameworkerror! " ,e);
14 } catch (InterruptedExceptione){
15 throw new OSGiStopException( " StopOSGiFrameworkerror! " ,e);
16 } finally {
17 if (logger.isInfoEnabled()){
18 if (succeed)logger.info( " OSGiFrameworkStopped! " );
19 else logger.info( " OSGiFrameworknotstop! " );
20 }
21 }
22 }
23 }



最后,还有一件事情,就是将FrameworkConfigListener配置到web.xml中:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 <!-- InitOSGiframework -->
2 < listener >
3 < listener-class > org.dbstar.osgi.web.launcher.FrameworkConfigListener </ listener-class >
4 </ listener >


让我们来测试一下吧,在Eclipse中新建一个Server:
打造一个基于OSGi的Web Application
打造一个基于OSGi的Web Application

另外,在OSGi-Web-Launcher项目的classpath中增加 org.eclipse.osgi_3.6.0.v20100128-1430.jar,并且在Java EE Module Dependencies中勾选这个jar,这样可以保证这个jar最终部署到Web Application的WEB-INF/lib目录下去。同样,还需要增加commons-logging.jar。

然后就可以启动这个Server查看效果了。

附上本文中提到的源 代码

为OSGi容器提供Web Application环境

本章叙述如何在OSGi容器中提供必要的Web Application环境,其中包括Servlet 2.4、Jsp 2.0和Commons-Logging相关的package,使得其他在OSGi容器中的bundle可以import。

为了在OSGi容器中提供export的package,一般有三种方式:

  1. 一个常规的bundle,自身包含必要的class,同时在Export-Package中声明。
  2. 一个Host为System Bundle的Fragment Bundle,同样也可以在Export-Package中声明导出的package,只要这个package中的class在System Bundle的ClassLoader中能load到。
  3. 通过启动Framework的配置项:org.osgi.framework.system.packages和 org.osgi.framework.system.packages.extra。OSGi 4.2规范中描述了这两个标准的配置项。在这两个配置项中描述的package都等同于在System Bundle中声明了export。


对于在Web Application中运行的OSGi容器,一些必要的环境是通过Web Container提供的,我们最好不要,也不应该用自己的类来替换,这包括了j2ee相关的jar,如servlet和jsp相关的jar等等。在一些 WebServer的实现中,会自动屏蔽Web Application的classpath中的j2ee相关的jar。

除了j2ee相关的jar之外,还有一些使用非常普遍的jar,比如说Apache commons一类,其中最常用的大概就是commons-lang.jar、commons-io.jar和commons-logging.jar 了,这些jar最好也有Web Container来提供,或者有必要的话,在Web Application中提供,而不是在OSGi容器中提供,这涉及到一些JVM层次的单例类,或者希望能由Web Application级别来统一实现和配置的环境,最常见的应用就是日志配置了。通过由Web Application提供的commons-logging来给OSGi容器中的环境使用,而commons-logging通过何种方式来实现,不需 要让OSGi内部知道。

至于导出package到OSGi的方式中,是采用第二种还是第三种,主要区别在于:第三种方式是加载framework时指定的,在其后的生命周期中不 可更改,而第二种方式则更符合OSGi动态加载的特性。

我采用第二种方式来给OSGi容器增加环境支持,具体操作很简单,以Servlet为例,首先编写一个文本文件,名字为:MANIFEST.MF,内容如 下:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 Manifest-Version: 1.0
2 Bundle-ManifestVersion: 2
3 Bundle-Name:ServletExtensionFragment
4 Bundle-SymbolicName:javax.servlet_extension ; singleton:=true
5 Bundle-Version: 2.4.0
6 Fragment-Host:system.bundle ; extension:=framework
7 Bundle-RequiredExecutionEnvironment:J2SE- 1.5
8 Export-Package:javax.servlet ; version="2.4.0",
9 javax.servlet.http ; version="2.4.0",
10 javax.servlet.resources ; version="2.4.0"
11 Bundle-Vendor:dbstar

注意其中关键的header属性,Fragment-Host: system.bundle; extension:=framework
这样写才能保证这个Fragment Bundle在各种OSGi Framework实现中都能兼容。
保存以后,将这个文件放置到一个名字为META-INF的目录中,然后用jar命令打包成一个jar即可(或者用winrar打包,记得选择压缩方式为 zip,在打包后将zip后缀名改成jar,我通常都是这么干的)。

Jsp的MANIFEST.MF:

<!--<br /> <br /> Code highlighting produced by Actipro CodeHighlighter (freeware)<br /> http://www.CodeHighlighter.com/<br /> <br /> -->1 Manifest-Version: 1.0
2 Bundle-ManifestVersion: 2
3 Bundle-Name:JspExtensionFragment
4 Bundle-SymbolicName:javax.servlet.jsp_extension ; singleton:=true
5 Bundle-Version: 2.0.0
6 Bundle-Vendor:dbstar
7 Fragment-Host:system.bundle ; extension:=framework
8