1. 程式人生 > >打造一個基於OSGi的Web Application

打造一個基於OSGi的Web Application

本章將建立一個Web Application專案,並描述如何在此應用中啟動OSGi。首先,在Eclipse中建立一個Dynamic Web Project,名字為OSGi-Web,Context root為osgi。這個專案只作為部署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 publicabstractclass ServiceLoader { 9 publicfinalstatic<E> Class<E> load(Class<E> clazz) throws IOException, ClassNotFoundException {10 return load(clazz, Thread.currentThread().getContextClassLoader());11     }12 13     @SuppressWarnings("unchecked"
)14 publicfinalstatic<E> Class<E> load(Class<E> clazz, ClassLoader classLoader) throws IOException,15             ClassNotFoundException {16         String resource ="META-INF/services/"+ clazz.getName();17         InputStream in = classLoader.getResourceAsStream(resource);18 if (in ==nullreturnnull;19 20 try {21             BufferedReader reader =new BufferedReader(new InputStreamReader(in));22             String serviceClassName = 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 (Exception e) {4 thrownew IllegalArgumentException("FrameworkFactory service load error.", e);5             }6 if (frameworkFactoryClass ==null) {7 thrownew IllegalArgumentException("FrameworkFactory service not found.");8             }

例項化FrameworkFactory:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->1             FrameworkFactory frameworkFactory;2 try {3                 frameworkFactory = frameworkFactoryClass.newInstance();4             } catch (Exception e) {5 thrownew IllegalArgumentException("FrameworkFactory instantiation error.", 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("Load Framework configuration: ["); 7 for (Object key : configuration.keySet()) { 8                         logger.info("/t"+ key +" = "+ configuration.get(key)); 9                     }10                     logger.info("]");11                 }12             } catch (Exception e) {13 thrownew IllegalArgumentException("Load Framework configuration error.", 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 privatestatic Map<Object, Object> loadFrameworkConfig(ServletContext context) throws MalformedURLException { 3         String configLocation = context.getInitParameter(CONTEXT_PARAM_OSGI_CONFIG_LOCATION); 4 if (configLocation ==null) configLocation = DEFAULT_OSGI_CONFIG_LOCATION; 5 elseif (!configLocation.startsWith("/")) configLocation ="/".concat(configLocation); 6  7         Properties config =new Properties(); 8 try { 9 // 載入配置項10             config.load(context.getResourceAsStream(configLocation));11 if (logger.isInfoEnabled()) logger.info("Load Framework configuration from: "+ configLocation);12         } catch (IOException e) {13 if (logger.isWarnEnabled()) logger.warn("Load Framework configuration error from: "+ configLocation, e);14         }15 16         String storageDirectory = 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("Use Framework Storage: "+ 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 (BundleException e) {13 thrownew OSGiStartException("Start OSGi Framework error!", e);14             } catch (IOException e) {15 thrownew OSGiStartException("Init OSGi Framework error", e);16             }

在initFramework方法中,主要做兩件事情,一是將當前的ServletContext作為一個service註冊到OSGi容器中去:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->1 privatestaticvoid registerContext(BundleContext bundleContext, ServletContext servletContext) {2         Properties properties =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 privatestaticvoid initFramework(Framework framework, ServletContextEvent event) throws IOException { 3         BundleContext bundleContext = framework.getBundleContext(); 4         ServletContext servletContext = event.getServletContext(); 5  6 // 將ServletContext註冊為服務 7         registerContext(bundleContext, servletContext); 8  9         File file = bundleContext.getDataFile(".init");10 if (!file.isFile()) { // 第一次初始化11 if (logger.isInfoEnabled()) logger.info("Init Framework");12 13             String pluginLocation = servletContext.getInitParameter(CONTEXT_PARAM_OSGI_PLUGINS_LOCATION);14 if (pluginLocation ==null) pluginLocation = DEFAULT_OSGI_PLUGINS_LOCATION;15 elseif (!pluginLocation.startsWith("/")) pluginLocation ="/".concat(pluginLocation);16 17 // 安裝bundle18             File bundleRoot =new File(servletContext.getRealPath(pluginLocation));19 if (bundleRoot.isDirectory()) {20 if (logger.isInfoEnabled()) logger.info("Load Framework bundles from: "+ pluginLocation);21 22                 File bundleFiles[] = bundleRoot.listFiles(new FilenameFilter() {23 publicboolean accept(File dir, String name) {24 return name.endsWith(".jar");25                     }26                 });27 28 if (bundleFiles !=null&& bundleFiles.length >0) {29 for (File bundleFile : bundleFiles) {30 try {31                             bundleContext.installBundle(bundleFile.toURL().toExternalForm());32 if (logger.isInfoEnabled()) logger.info("Install bundle success: "+ bundleFile.getName());33                         } catch (Throwable e) {34 if (logger.isWarnEnabled()) logger.warn("Install bundle error: "+ bundleFile, e);35                         }36                     }37                 }38 39 for (Bundle bundle : 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("Start bundle: "+ bundle);45                             } catch (Throwable e) {46 if (logger.isWarnEnabled()) logger.warn("Start bundle error: "+ bundle, e);47                             }48                         }49                     }50                 }51             }52 53 new FileWriter(file).close();54 if (logger.isInfoEnabled()) logger.info("Framework inited.");55         }56     }

以上就是啟動OSGi容器的過程,相比較而言,停止容器就簡單多了:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />--> 1 publicvoid contextDestroyed(ServletContextEvent event) { 2 if (framework !=null) { 3 if (logger.isInfoEnabled()) logger.info("Stopping OSGi Framework"); 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 (BundleException e) {13 thrownew OSGiStopException("Stop OSGi Framework error!", e);14             } catch (InterruptedException e) {15 thrownew OSGiStopException("Stop OSGi Framework error!", e);16             } finally {17 if (logger.isInfoEnabled()) {18 if (succeed) logger.info("OSGi Framework Stopped!");19 else logger.info("OSGi Framework not stop!");20                 }21             }22         }23     }

最後,還有一件事情,就是將FrameworkConfigListener配置到web.xml中:

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

讓我們來測試一下吧,在Eclipse中新建一個Server:另外,在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環境,其中包括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: Servlet Extension Fragment 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: Jsp Extension Fragment 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 Bundle-RequiredExecutionEnvironment: J2SE-1.5 9 Export-Package: javax.servlet.jsp;version="2.0.0",10  javax.servlet.jsp.el;version="2.0.0",11  javax.servlet.jsp.resources;version="2.0.0",12  javax.servlet.jsp.tagext;version="2.0.0"

commons-logging的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: Commons Logging Extension Fragment 4 Bundle-SymbolicName: org.apache.commons.logging_extension;singleton:=true 5 Bundle-Version: 1.1.1 6 Bundle-Vendor: dbstar 7 Fragment-Host: system.bundle; extension:=framework 8 Bundle-RequiredExecutionEnvironment: J2SE-1.5 9 Export-Package: org.apache.commons.logging;version="1.1.1",10  org.apache.commons.logging.impl;version="1.1.1"

因為我用的是commons-logging-1.1.1.jar,所以version寫的是1.1.1,大家可以修改成自己所使用的jar的版本。將上面生成的三個jar放到OSGi-Web專案的WEB-INF/osgi/plugins目錄下面。還記得我在上一章建立的那個Tomcat Server麼,clean一次,新的jar會部署到Tomcat中去,然後就可以執行Server了。至於為什麼是clean而不是publish,區別在於clean會清除所有OSGi容器創建出來的檔案,這樣下次啟動OSGi時就會做一個install bundle的事情,而publish不會自動install新加進去的bundle。如果你使用的是equinox,那麼你可以在控制檯中看到Syetem Bundle現在多了幾個Fragments,檢視一下Servlet Bundle,會顯示下列資訊,表示servlet 2.4的package在OSGi容器中已經可用了:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->osgi> bundle 2javax.servlet_extension_2.4.0[2]  Id=2, Status=RESOLVED    Data Root=D:/dbstar/workspaces/OSGi/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/OSGi-Web/WEB-INF/osgi/configuration/org.eclipse.osgi/bundles/2/data  No registered services.  No services in use.  Exported packages    javax.servlet; version="2.4.0"[exported]    javax.servlet.http; version="2.4.0"[exported]    javax.servlet.resources; version="2.4.0"[exported]  No imported packages  Host bundles    org.eclipse.osgi_3.6.0.v20100128-1430[0]  No named class spaces  No required bundles