本文介紹開發一個web console以管理OSGi框架及bundles的實現方法,可適用於實現了OSGi規範的Equinox、Felix等開源框架。並介紹瞭如何把OSGi框架作爲一個組件嵌入到現有的未基於OSGi開發的Web應用當中,在Web應用中可獲取OSGi中的Service以增加應用的靈活性。
本文適用於具有OSGi基本知識的人員閱讀。
本例所述源代碼在http://download.csdn.net/detail/tsun7263/4167550或http://ishare.iask.sina.com.cn/f/23642252.html下載。
1 利用OSGi 開源框架本身提供的方法啓動OSGi Framework
啓動OSGi Framework有多種方法,如Equinox中就包含啓動OSGi Framework的方法,可在服務器端開啓一個命令行窗口,用於對bundles進行操作。
啓動Equinox的代碼:
// 根據要加載的bundle組裝出類似a.jar@start,b.jar@3:start這樣格式的osgibundles字符串來
StringosgiBundles="";
//配置Equinox的啓動
FrameworkProperties.setProperty("osgi.noShutdown","true");
FrameworkProperties.setProperty("eclipse.ignoreApp","true");
FrameworkProperties.setProperty("osgi.bundles.defaultStartLevel","4");
FrameworkProperties.setProperty("osgi.bundles", osgiBundles);
// 根據需要設置bundle所在的路徑
String bundlePath="";
// 指定要加載的plugins所在的目錄
FrameworkProperties.setProperty("osgi.syspath", bundlePath);
// 調用EclipseStarter,完成容器的啓動,指定configuration目錄
try {
EclipseStarter.run(new String[]{"-configuration","configuration","-console"},null);
}catch (Exception e) {
e.printStackTrace();
}
// 通過EclipeStarter獲得BundleContext
context = EclipseStarter.getSystemBundleContext();
停止Equinox的代碼:
try {
EclipseStarter.shutdown();
context=null;
System.err.println("osgi exit");
}
catch (Exception e) {
System.err.println("停止equinox容器時出現錯誤:" + e);
e.printStackTrace();
}
利用OSGi實現框架本身提供的方法啓動OSGi Framework的方法需要針對特定的OSGi實現編寫代碼,不具有通用性。
2 通用的OSGi Framework啓動方法
設計思路:
1、建立一個ServletContextListener,監聽到應用啓動時,啓動OSGi Framework。
2、根據OSGi 4.2規範,實現jar中放置一個文件META-INF/services/org.osgi.framework.launch.FrameworkFactory,這個文件中設置了實際的FrameworkFactory實現類的類名。通過讀取這個文件加載FrameworkFactory。
3、利用OSGi Framework中提供的API執行啓動等各種操作。
這種實現方式的好處是代碼具有通用性,底層所使用的具體OSGi實現框架對應用是透明的,可以無需修改代碼和配置即可實現OSGi框架的替換。
2.1 OSGi配置文件osgi.properties
osgi.properties爲如下格式的文件:
#bundles的默認存放路徑
osgi.bundles.defaultPath=WEB-INF/bundles
#要加載bundles,根據要加載的bundle組裝出類似a.jar@start,b.jar@3:start這樣格式的osgibundles字符串來
osgi.bundles=dict-query.jar,local-dict-query.jar@6:start,remote-dict-query.jar@5:start
#bundles的默認StartLevel
osgi.bundles.defaultStartLevel=4
#framework的StartLevel
osgi.startLevel=6
2.2 osgi.properties的讀取
BundleConfig.java 用於存放osgi.bundles中單條數據,BundleConfig.java用於存放osgi.properties中數據。
2.3 ServletContextListener的實現類ContainerListener
package demo.osgi;
import java.io.InputStream;
import java.util.Properties;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class ContainerListener implements ServletContextListener{
privatestatic final Log log = LogFactory.getLog(OSGiAdmin.class);
@Override
publicvoid contextInitialized(ServletContextEvent event) {
try{
StringosgiConfigPath =event.getServletContext().getInitParameter("osgiConfig");
if(osgiConfigPath == null) osgiConfigPath = "/osgi.properties";
Propertiesprop = new Properties();
InputStreamis = ContainerListener.class.getResourceAsStream(osgiConfigPath);
prop.load(is);
is.close();
OSGiConfigosgiConfig = new OSGiConfig();
osgiConfig.load(prop,event.getServletContext());
OSGiAdmin.startup(osgiConfig,event.getServletContext());
}catch (Exception e) {
log.error("啓動OSGi框架失敗:" +e.getMessage(), e);
}
}
@Override
publicvoid contextDestroyed(ServletContextEvent event) {
try{
OSGiAdmin.shutdown();
}catch (Exception e) {
log.error("卸載OSGi框架失敗:" +e.getMessage(), e);
}
}
}
2.4 web.xml中的設置
在web.xml中增加以下內容:
<context-param>
<param-name>osgiConfig</param-name>
<param-value>/osgi.properties</param-value>
</context-param>
<listener>
<listener-class>demo.osgi.ContainerListener</listener-class>
</listener>
2.5 OSGiAdmin.java實現OSGiFramework的啓動和停止
package demo.osgi;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import com.googlecode.transloader.DefaultTransloader;
import com.googlecode.transloader.ObjectWrapper;
import com.googlecode.transloader.Transloader;
import com.googlecode.transloader.clone.CloningStrategy;
publicclassOSGiAdmin {
privatestaticfinal Log log = LogFactory.getLog(OSGiAdmin.class);
privatestatic OSGiConfig osgiConfig;
privatestatic Framework framework =null;
publicstaticvoid startup(OSGiConfig osgiConfig,ServletContext servletContext) throws Exception {
if (log.isInfoEnabled())log.info("正在啓動OSGi框架...");
OSGiAdmin.osgiConfig = osgiConfig;
//加載FrameworkFactory.class
Class<FrameworkFactory>frameworkFactoryClass =loadClass(FrameworkFactory.class);
if (frameworkFactoryClass ==null)thrownew Exception("加載classFrameworkFactory失敗");
FrameworkFactoryframeworkFactory = (FrameworkFactory)frameworkFactoryClass.newInstance();
Map<String,String> cofiguration =newHashMap<String, String>();
cofiguration.put(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, String.valueOf(osgiConfig.getFrameworkStartLevel()));
framework =frameworkFactory.newFramework(cofiguration);
framework.init();
FrameworkStartLevelframeworkStartLevel =framework.adapt(FrameworkStartLevel.class);
frameworkStartLevel.setInitialBundleStartLevel(osgiConfig.getBundlesDefaultStartLevel());
log.info("osgi bundles默認啓動級別:" + osgiConfig.getBundlesDefaultStartLevel());
log.info("osgi framework啓動級別:" + osgiConfig.getFrameworkStartLevel());
initFramework(framework, servletContext);
List<BundleConfig>bundleConfigs = osgiConfig.getBundleConfigs();
for (BundleConfig bundleConfig : bundleConfigs) {
try {
log.info("裝載bundle " + bundleConfig.getLocationUrl() +" ...");
Bundlebundle = installBundle(bundleConfig.getLocationUrl());
BundleStartLevelbundleStartLevel = bundle.adapt(BundleStartLevel.class);
int lvl = bundleConfig.getStartLevel();
if (bundleConfig.isAutoStart()
&&(bundle.getHeaders().get(Constants.BUNDLE_ACTIVATOR) !=null || bundle.getHeaders().get("Service-Component") !=null)) {
if (lvl > osgiConfig.getFrameworkStartLevel()) {
//確保bundle的startLevel在自動啓動的範圍內
lvl= osgiConfig.getFrameworkStartLevel();
}
}
else {
if (lvl <= osgiConfig.getFrameworkStartLevel()) {
//設置bundle的startLevel大於framework的startLevel以禁止自動啓動
lvl= osgiConfig.getFrameworkStartLevel() + 1;
}
}
log.info("startLevel:" + lvl);
bundleStartLevel.setStartLevel(lvl);
bundle.start(Bundle.START_ACTIVATION_POLICY);
log.info("裝載bundle " + bundleConfig.getLocationUrl() +"成功");
}catch (BundleException e) {
log.info("裝載bundle " + bundleConfig.getLocationUrl() +"失敗:" +e.getMessage(), e);
}
}
//啓動Framework
framework.start();
log.info("osgi framework啓動後的啓動級別:" + frameworkStartLevel.getStartLevel());
if (log.isInfoEnabled())log.info("啓動OSGi框架成功");
}
@SuppressWarnings("unchecked")
privatestatic <T> Class<T> loadClass(Class<T> clazz) throws IOException, ClassNotFoundException {
ClassLoaderclassLoader = Thread.currentThread().getContextClassLoader();
Stringresource ="META-INF/services/" +clazz.getName();
InputStreamin = classLoader.getResourceAsStream(resource);
if (in ==null)returnnull ;
try {
BufferedReaderreader =new BufferedReader(new InputStreamReader(in));
StringserviceClassName = reader.readLine();
return(Class<T>)classLoader.loadClass(serviceClassName);
}finally {
in.close();
}
}
publicstatic OSGiConfig getOSGiConfig() {
returnOSGiAdmin.osgiConfig;
}
privatestaticvoid registerContext(BundleContextbundleContext, ServletContext servletContext) {
Dictionary<String,String> properties =newHashtable<String, String>();
properties.put("ServerInfo",servletContext.getServerInfo());
properties.put("ServletContextName",servletContext.getServletContextName());
properties.put("MajorVersion", String.valueOf(servletContext.getMajorVersion()));
properties.put("MinorVersion", String.valueOf(servletContext.getMinorVersion()));
bundleContext.registerService(ServletContext.class.getName(), servletContext, properties);
}
privatestaticvoid initFramework(Frameworkframework, ServletContext servletContext) throws IOException {
BundleContextbundleContext = framework.getBundleContext();
// 將ServletContext註冊爲服務
registerContext(bundleContext,servletContext);
}
publicstaticvoid startup() throws Exception {
framework.start();
}
publicstaticvoid shutdown() throws Exception {
if (framework == null)return;
if (log.isInfoEnabled())log.info("正在停止OSGi框架...");
if (framework.getState() == Framework.ACTIVE)framework.stop();
framework.waitForStop(0);
//framework = null;
log.info("OSGi框架已停止");
}
privatestatic BundleContext getBundleContext() {
returnframework.getBundleContext();
}
}
2.6 效果
把幾個測試bundles放在WEB-INF/bundles下面。本例中是根據《OSGi 原理與最佳實踐》中字典查詢的示例編寫的。
dict-query.jar封裝了字典查詢的接口
local-dict-query.jar是字典查詢接口的本地實現
remote-dict-query.jar是字典查詢的遠程實現
啓動應用服務器後,在後臺看到如下效果:
INFO 2012-03-23 22:24:34,375 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:43) -正在啓動OSGi框架...
INFO 2012-03-23 22:24:35,218 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:60) - osgi bundles 默認啓動級別:4
INFO 2012-03-23 22:24:35,218 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:61) - osgi framework 啓動級別:6
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:69) -裝載bundlefile:/E:/java/workspace1/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/osgiWebConsole/WEB-INF/bundles/dict-query.jar...
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:87) - startLevel:7
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:91) -裝載bundlefile:/E:/java/workspace1/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/osgiWebConsole/WEB-INF/bundles/dict-query.jar成功
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:69) -裝載bundlefile:/E:/java/workspace1/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/osgiWebConsole/WEB-INF/bundles/remote-dict-query.jar...
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:87) - startLevel:5
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:91) -裝載bundlefile:/E:/java/workspace1/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/osgiWebConsole/WEB-INF/bundles/remote-dict-query.jar成功
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:69) -裝載bundlefile:/E:/java/workspace1/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/osgiWebConsole/WEB-INF/bundles/local-dict-query.jar...
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:87) - startLevel:6
INFO 2012-03-23 22:24:35,234 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:91) -裝載bundlefile:/E:/java/workspace1/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/osgiWebConsole/WEB-INF/bundles/local-dict-query.jar成功
INFO 2012-03-23 22:24:35,328 demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:99) - osgi framework 啓動後的啓動級別:6
INFO 2012-03-23 22:24:35,328demo.osgi.OSGiAdmin.startup(OSGiAdmin.java:101) -啓動OSGi框架成功
3 OSGi Web Console的實現
在OSGiAdmin.java中提供對Framewo、Bundle的操作,從Web頁面上調用。
以下對Bundle操作的代碼:
publicstaticvoid startBundle(long id)throws BundleException {
Bundlebundle =getBundle(id);
if (bundle ==null)return;
if (bundle.getState() != Bundle.UNINSTALLED && bundle.getState() != Bundle.ACTIVE) {
bundle.start();
}
}
publicstaticvoid stopBundle(long id)throws BundleException {
Bundle bundle =getBundle(id);
if (bundle ==null)return;
if (bundle.getState() == Bundle.ACTIVE) {
bundle.stop();
}
}
publicstatic Bundle installBundle(String location) throws BundleException {
BundleContextbundleContext =getBundleContext();
Bundlebundle = bundleContext.installBundle(location);
return bundle;
}
publicstaticvoid uninstallBundle(long id)throws BundleException {
Bundle bundle =getBundle(id);
if (bundle ==null)return;
if (bundle.getState() == Bundle.UNINSTALLED)return;
if (bundle.getState() == Bundle.ACTIVE) {
bundle.stop();
}
bundle.uninstall();
}
在Web頁面控制OSGi Framework和Bundles,在osgi_console.jsp頁面進行查看和操作,效果如下:
4 OSGi嵌入到Web應用的實現
對於既有的Web應用改造成純OSGi應用可能會是一件耗時的工作,需要進行模塊的劃分等改造工作。在既有的Web應用中嵌入OSGi框架是一種可選的方法,把OSGi框架作爲服務的提供者,Web應用調用服務完成業務邏輯的運算。
在OSGiAdmin.java中加入獲取service的接口:
publicstatic ObjectgetService(Class<?> clazz){
ServiceReference<?> serviceRef =context.getServiceReference(clazz);
Object obj =null;
if(serviceRef !=null) {
obj =context.getService(serviceRef);
context.ungetService(serviceRef);
}
return obj;
}
在web客戶端進行調用,如dict_query.jsp中的調用:
Object obj =OSGiAdmin.getService(QueryService.class);
if (obj !=null) {
QueryServicqueryServic = (QueryServic)obj;
Stringvalue = queryService.queryWord(key);
if (value ==null) value="";
out.println(value);
}
else {
out.println("服務不存在");
}
需要把dict-query.jar同時放入WEB-INF/lib下。運行時出現ClassCastException,這是什麼原因呢?
瞭解OSGi的人應該知道,OSGi有自己的類加載方式,形成OSGi環境;而Web應用是使用的應用服務器的類加載方式,是非OSGi環境。Web應用中的demo.osgi.dictquery.QueryService是由應用服務器的ClassLoader加載的,OSGi框架中的demo.osgi.dictquery.QueryService是由OSGi的ClassLoader加載的,雖然類的名稱是一樣的,但因爲是有不同ClassLoader加載的,在jvm中認爲這是兩個不同的Class,所以會出現ClassCastException。
解決方法有:
(1)在Web應用的客戶端不使用強類型,僅得到服務的Object類型的實例,通過反射進行調用。
(2)把OSGi中Object複製到非OSGi環境中,這樣就可以使用服務接口的強類型進行引用,但要求客戶端能夠加載服務接口的Class。
此處介紹一下TransLoader開源框架(http://code.google.com/p/transloader/)。它可用於ClassLoader間對象的複製、通過反射調用方法,最初是爲OSGi環境與非OSGi環境通信而設計,但也適用於其他ClassLoader之間傳輸對象的情景。TransLoader開源框架可實現對象的深度複製、封裝實例的最少複製。
TransLoader開源框架可以解決我們遇到的這個問題,對OSGiAdmin.java的getService方法改造如下:
publicstatic <T> T getService(Class<T>clazz) {
T result =null;
BundleContext bundleContext =getBundleContext();
ServiceReference<?> serviceRef =bundleContext.getServiceReference(clazz.getName());
if (null != serviceRef) {
Object resultObj =bundleContext.getService(serviceRef);
if (resultObj !=null) {
Transloader transloader =getTransloader();
ObjectWrapper resultWrapped =transloader.wrap(resultObj);
if (resultWrapped.isInstanceOf(clazz.getName())) {
result = (T)resultWrapped.makeCastableTo(clazz);
}
}
}
if (serviceRef !=null)bundleContext.ungetService(serviceRef);
return result;
}
Web應用的客戶端代碼修改爲:
QueryService queryService =OSGiAdmin.getService(QueryService.class);
if (queryService !=null) {
Stringvalue = queryService.queryWord(key);
if (value ==null) value="";
out.println(value);
}
else {
out.println("服務不存在");
}
啓動應用服務器後,在dict_query.jsp頁面輸入“temp”進行查詢,顯示如下:
在OSGi頁面停止bundle remote-dict-query.jar,dict_query.jsp頁面顯示爲:
這樣就實現了bundle的熱插拔,在不重啓應用服務器的情況下實現服務的切換。
在本示例基礎上,可以增加bundle的客戶端上傳等功能,實現更加實用的功能。
本文檔參考了網絡上《打造一個基於OSGi的WebApplication——在WebApplication中啓動OSGi》、《從外部啓動Equinox》、《基於 Equinox 的 OSGi Console的研究和探索》等文章,在此致謝。