Java Web應用整合OSGI

來源:互聯網
上載者:User

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 bundlecom.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

BundleImport-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時,首先調用BundleActivatorstart方法,對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判斷是否將客戶請求分發到這個Bundleservlet-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
  • 可以調用Bundlebundle.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狀態更新後,會調用BundleListenerbundleChanged回調方法。然後我們可以在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);        }     } });

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.