1. 程式人生 > >Java Web應用集成OSGI

Java Web應用集成OSGI

att servlet 不可見 logo install 服務器 system cati 所在

對OSGI的簡單理解

就像Java Web應用程序需要運行在Tomcat、Weblogic這樣的容器中一樣。程序員開發的OSGI程序包也需要運行在OSGI容器中。目前主流的OSGI容器包括:Apache Felix以及Eclipse Equinox。OSGI程序包在OSGI中稱作BundleBundle的整個生命周期都交與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

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);
        }
     }
 });

Java Web應用集成OSGI