Java Web應用集成OSGI
對OSGI的簡單理解
就像Java Web應用程序需要運行在Tomcat、Weblogic這樣的容器中一樣。程序員開發的OSGI程序包也需要運行在OSGI容器中。目前主流的OSGI容器包括:Apache Felix以及Eclipse Equinox。OSGI程序包在OSGI中稱作Bundle
。 Bundle
的整個生命周期都交與OSGI容器進行管理。可以在不停止服務的情況下,對Bundle
進行加載和卸載,實現熱部署。 Bundle
對於外部程序來說就是一個黑盒。他只是向OSGI容器中註冊了供外部調用的服務接口,至於實現則對外部不可見。不同的Bundle
之間的調用,也需要通過OSGI容器來實現。
Bundle如何引入jar
剛才說到Bundle
是一個黑盒,他所有實現都包裝到了自己這個“盒子”中。在開發Bundle
時,避免不了引用一些比如Spring、Apache commons等開源包。在為Bundle
打包時,可以將當前Bundle
依賴jar與Bundle
的源碼都打包成一個包(all-in-one)。這種打包結果就是打出的包過大,經常要幾兆或者十幾兆,這樣當然我們是不可接受的。下面就介紹一種更優的做法。
Bundle與OSGI容器的契約
___Bundle
可以在MANIFEST.MF
配置文件中聲明他要想運行起來所要的包以及這些包的版本 !!!而OSGI容器在加載Bundle
時會為Bundle
Bundle
所需要的包 !!!___在啟動OSGI容器時,需要在OSGI配置文件中定義org.osgi.framework.system.packages.extra
,屬性。這個屬性定義了 OSGI容器能提供的包以及包的版本。OSGI在加載Bundle
時,會將他自己能提供的包以及版本與Bundle所需要的包以及版本列表進行匹配。如果匹配不成功則直接拋出異常:
Unable to execute command on bundle 248: Unresolved constraint in bundle com.osgi.demo2 [248]: Unable to resolve 248.0: missing requirement [248.0] osgi .wiring.package; (&(osgi.wiring.package=org.osgi.framework)(version>=1.8.0)(!(version>=2.0.0)))
也可能加載Bundle
通過,但是運行Bundle
時報ClassNotFoundException
。這些異常都由於配置文件沒配置造成的。理解了配置文件的配置方法,就能解決60%的異常。
Import-Package
在Bundle
的Import-Package
屬性中通過以下格式配置:
<!--pom.xml--> <Import-Package> javax.servlet, javax.servlet.http, org.xml.sax.*, org.springframework.beans.factory.xml;org.springframework.beans.factory.config;version=4.1.1.RELEASE, org.springframework.util.*;version="[2.5,5.0]" </Import-Package>
- 包與包之間通過逗號分隔
- 可以使用*這類的通配符,表示這個包下的所有包。如果不想使用通配符,則同一個包下的其他包彼此之間可以使用
;
分隔。 - 如果需要指定包的版本則在包後面增加
;version="[最低版本,最高版本]"
。其中[
表示大於等於、]
表示小於等於、)
表示小於。
org.osgi.framework.system.packages.extra
語法與Impirt-Package
基本一致,只是org.osgi.framework.system.packages.extra
不支持通配符。
- 錯誤的方式
org.springframework.beans.factory.*;version=4.1.1.RELEASE
- 正確的方式:
org.springframework.beans.factory.xml;org.springframework.beans.factory.config;version=4.1.1.RELEASE,
Class文件加載
在我們平時開發中有些情況下加載一個Class會使用this.getClassLoader().loadClass
。但是通過這種方法加載Bundle
中所書寫的類的class
會失敗,會報ClassNotFoundException
。在Bundle
需要使用下面的方式來替換classLoader.loadClass
方法
public void start(BundleContext context) throws Exception { Class classType = context.loadClass(name); }
Bundle中加載Spring配置文件時的問題
由於Bundle
加載Class
的特性,會導致在加載Spring配置文件時報錯。所以需要將Spring啟動所需要的ClassLoader進行更改,使其調用BundleContext.loadClass
來加載Class。
String xmlPath = ""; ClassLoader classLoader = new ClassLoader(ClassUtils.getDefaultClassLoader()) { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { return currentBundle.loadClass(name); } catch (ClassNotFoundException e) { return super.loadClass(name); } } }; DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.setBeanClassLoader(classLoader); GenericApplicationContext ctx = new GenericApplicationContext(beanFactory); ctx.setClassLoader(classLoader); DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader) { @Override public void setClassLoader(ClassLoader classLoader) { if (this.getClassLoader() == null) { super.setClassLoader(classLoader); } } }; ctx.setResourceLoader(resourceLoader); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx); reader.loadBeanDefinitions(xmlPath); ctx.refresh();
Web應用集成OSGI
這裏選用了Apache Felix
來開發,主要是因為Apache Felix
是Apache的頂級項目。社區活躍,對OSGI功能支持比較完備,並且文檔例子比較全面。 其實OSGI支持兩種方式來部署Bundle
。
- 單獨部署OSGI容器,通過OSGI自帶的Web中間件(目前只有jetty)來對外提供Web服務
- 將OSGI容器嵌入到Web應用中,然後就可以使用Weblogic等中間件來運行Web應用
從項目的整體考慮,我們選用了第二種方案。
BundleActivator開發
開發Bundle
時,首先需要開發一個BundleActivator
。OSGI在加載Bundle
時,首先調用BundleActivator
的start
方法,對Bundle
進行初始化。在卸載Bundle
時,會調用stop
方法來對資源進行釋放。
public void start(BundleContext context) throws Exception; public void stop(BundleContext context) throws Exception;
在start
方法中調用context.registerService
來完成對外服務的註冊。
Hashtable props = new Hashtable(); props.put("servlet-pattern", new String[]{"/login","/logout"}) ServiceRegistration servlet = context.registerService(Servlet.class, new DispatcherServlet(), props);
- context.registerService方法的第一個參數表示服務的類型,由於我們提供的是Web請求服務,所以這裏的服務類型是一個
javax.servlet.Servlet
,所以需要將javax.servlet.Servlet
傳入到方法中 - 第二個參數為服務處理類,這裏配置了一個路由Servlet,其後會有相應的程序來處理具體的請求。
- 第三個參數為
Bundle
對外提供服務的屬性。在例子中,在Hashtable
中定義了Bundle
所支持的servlet-pattern
。OSGI容器所在Web應用通過Bundle
定義的servlet-pattern
判斷是否將客戶請求分發到這個Bundle
。servlet-pattern
這個名稱是隨意起的,並不是OSGI框架要求的名稱。
應用服務集成OSGI容器
- 首先工程需要添加如下依賴
<dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.framework</artifactId> <version>5.6.10</version> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.http.bundle</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.http.bridge</artifactId> <version>3.0.18</version> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.http.proxy</artifactId> <version>3.0.0</version> </dependency>
- 然後在
web.xml
中添加
<listener> <listener-class>org.apache.felix.http.proxy.ProxyListener</listener-class> </listener>
- 開發
ServletContextListener
用以初始化並啟動OSGI容器 請參考Apache Felix
提供的例子程序。例子中提供的ProvisionActivator
會掃描/WEB-INF/bundles/
,加載其中的Bundle
包。(當然例子中提供的ProvisionActivator並不帶有Bundle
自動發現註冊等機制,這些邏輯需要自行增加。請參照後續的Bundle自動加載章節)
路由開發
通過上面的配置,只是將OSGI容器加載到了Web應用中。還需要修改Web應用程序路由的代碼。
- 在
Bundle
加載到OSGI容器中後,可以通過bundleContext.getBundles()
方法獲取到OSGI容器中的所有已經加載的Bundle
。 - 可以調用
Bundle
的bundle.getRegisteredServices()
方法獲取到該Bundle
對外提供的所有服務服務。getRegisteredServices
方法返回ServiceReference
的數組。前文中我們調用context.registerService(Servlet.class, new DispatcherServlet(), props)
我們已經註冊了一個服務,getRegisteredServices
返回的數據只有一個ServiceReference
對象。 - 獲取
Bundle
所能提供的服務 可以通過ServiceReference
對象的getProperty
方法獲取context.registerService
中傳入的props
中的值。這樣我們就能通過調用ServiceReference.getProperty
方法獲取到該Bundle
所能提供的服務。 - 通過上面提供的接口,我們可以將
Bundle
對應ServiceReference
以及Bundle
對應的servlet-pattern
進行緩存。當用戶請求進入到應用服務器後,通過緩存的servlet-pattern
可以判斷Bundle
是否能提供用戶所請求的服務,如果可以提供通過下面的方式,來調用Bundle
所提供的服務。
ServiceReference sr = cache.get(bundleName); HttpServlet servlet = (HttpServlet) this.bundleContext.getService(sr); servlet.service(request, response);
Bundle自動加載
在Apache Felix
例子中提供的ProvisionActivator
,只會在系統啟動時加載/WEB-INF/bundles/
目錄下的Bundle
。當文件夾下的Bundle
文件有更新時,並不會自動更新OSGI容器中的Bundle
。所以Bundle
自動加載的邏輯,需要我們自己增加。下面提供實現的思路:
- 在第一次加載文件夾下的
Bundle
時,記錄Bundle
包所對應的最後的更新時間。 - 在程序中創建一個獨立線程,用以掃描
/WEB-INF/bundles/
目錄,逐個的比較Bundle
的更新時間。如果與內存中的不相符合,則從OSGI中獲取Bundle
對象然後調用其stop
以及uninstall
方法,將其從OSGI容器中卸載。 - 卸載後,再調用
bundleContext.installBundle
以及bundle.start
將最新的Bundle
加載到OSGI容器中
BundleListener
最後一個問題,通過上面的方式,可以實現Bundle
的自動加載。但是剛才我們介紹了,在路由程序中,我們會緩存OSGI容器中所有的Bundle
所對應的ServiceReference
以及所有Bundle
所對應的servlet-pattern
。所以Bundle
自動更新後,我們還需要將路由程序中的緩存同步的進行更新。 可以通過向bundleContext
中註冊BundleListener
,當OSGI容器中的Bundle
狀態更新後,會調用BundleListener
的bundleChanged
回調方法。然後我們可以在bundleChanged
回調方法中書寫更新路由緩存的邏輯
this.bundleContext.addBundleListener(new BundleListener() { @Override public void bundleChanged(BundleEvent event) { if (event.getType() == BundleEvent.STARTED) { initBundle(event.getBundle()); } else if (event.getType() == BundleEvent.UNINSTALLED) { String name = event.getBundle().getSymbolicName(); indexes.remove(name); } } });
Java Web應用集成OSGI