深入理解OSGi WEB應用程序規范和GlassFish OSGi/WEB容器
相關文章:GlassFish OSGi-JavaEE (一) GlassFish與企業級OSGi開發
http://www.bianceng.cn/Programming/Java/201312/38601.htm
在Part1中,我們提到了企業級OSGi制定了一系列的規范來與JavaEE集成,其中,最具代表性的規范是OSGi WEB應用程序規范,這部分將帶領大家深入理解OSGi WEB應用程序規范和GlassFish OSGi/WEB容器。本文將分成以下幾個部分:
理解OSGi WEB應用程序規范
構建一個OSGi WEB應用程序
使用GlassFish 4部署OSGi WEB應用程序
深入剖析GlassFish OSGi/WEB容器
思考
在信息和網絡發達的今天,WEB應用程序已經非常得流行和普遍,一些任務關鍵型(Mission-Critical)的WEB應用程序每天都在高負荷地運行,很少有中斷,因為一次不經意的中斷可能造成數據的大規模丟失,以至損失大量的資金而造成嚴重的後果。這些任務關鍵型的WEB應用往往出現在證券和股票等相關的金融行業。現在,我們開始考慮一個場景: 幾個星期或者幾個月甚至幾年後,WEB應用的客戶或者提供商希望在WEB前端增加一些新的模塊或功能,但是,為了增加這些新的模塊,我們不能停止WEB應用,而且也不希望再次重構或者改變WEB應用的現有架構和模塊。這聽起來不可思議,對於這樣一個場景,至少應該停止應用程服務的實例吧。但是,客戶不會答應。另一方面,在當今大數據的時代,每一秒鐘都會有大量的數據進入我們的應用之中。那麼,如何解決這樣的場景?
一個可行的答案是: 使用OSGi WEB構建我們的應用。
WAB-OSGi WEB的核心
簡單地說,OSGi WEB應用程序規范(chapter 128 in the OSGi Enterprise Release 5 Specification[1])定義了OSGi WEB的全部內容。對於OSGi WEB應用程序,典型情況下,它由一個WEB應用程序Bundle(即Web Application Bundle,簡稱為WAB)所構成。
因此,首先我們需要理解WAB以及和WAR的區別。
WAB簡述
在Part1中,我們已經提到Bundle是OSGi中的基本部署和管理實體。所以,WAB首先是一個Bundle,必須提供成為Bundle的OSGi元數據(如, Bundle-SymbolicName, Bundle-Version…),其次,WAB與JavaEE中的WAR一樣,依然是服務於WEB應用程序,能夠使用Servlet 2.5或更高版本的Servlet規范,因此,WAB必須包含可訪問的WEB內容,具體的說,Java Servlet規范定義了一個WEB應用程序的結構並定義了一個基於JAR的文件格式(WAR),WAB必須包含WAR中的靜態和動態的內容。
進一步地,要成為一個WAB,需要在MANIFEST.MF文件中通過Import-Package來描述它的依賴,例如: 通過導入javax.servlet來使用Servlet的功能,另外,如果需要向外界提供服務,它也要通過Export-Package來導出服務所在的包。
我們能夠通過不同的方式來安裝WAB,例如,通過支持企業級OSGi的應用服務器所提供的命令行控制台(如,GlassFish Admin CLI),也可以通過程序的方式調用OSGi底層API來安裝WAB(如,BundleContext.installBundle)。無論哪一種方式,WAB安裝後,它的生命周期管理就像OSGi運行時的其他Bundle一樣。只不過WAB的生命周期被一個Web Extender跟蹤,一旦WAB准備服務WEB請求時,Web Extender需要將WAB中可訪問的WEB內容部署到WEB運行時。以後當WAB不再服務WEB請求時,Web Extender也需要將這些可訪問的WEB內容從WEB運行時卸載掉。
關於WAB的安裝,有一點需要額外說明,一個WEB應用程序能夠在開發階段通過工具(例如, Maven插件)被打包成WAB然後進行安裝,或者這個WEB應用程序能夠在Bundle安裝階段通過Web URL Handler對標准WAR進行轉換來透明地創建WAB。GlassFish 4已經實現了後一種機制,我將在後續章節詳細闡述。
關於Web Extender和Web URL Handler,它們都是OSGi WEB容器的一部分,我們將在後面章節詳細闡述。
從上面的敘述,我們已經看到了安裝WAB與安裝普通Bundle的明顯的不同之處: 除了安裝WAB到OSGi運行時,還需要將WAB中可訪問的WEB內容部署到WEB運行時。關於這一點,OSGi WEB應用程序規范定義了WAB的生命周期狀態圖,
圖1: WAB的生命周期狀態圖
摘自: OSGi Enterprise Release 5 Specification
我們將在後續章節中深入闡述圖1中的每個階段。
WAB定義
WAB本身就是一個OSGi Bundle,因此,對於標准OSGi Bundle的定義同樣適用於WAB,但是,WAB與標准OSGi Bundle本質的區別在於: WAB需要在MANIFEST.MF中定義Web-ContextPath屬性。Web-ContextPath屬性定義了這個WEB應用程序訪問的上下文路徑(Context Path)[2],在WEB服務器上,這個WEB應用程序中所有可訪問的資源都要相對於這個上下文路徑。例如, 如果在MANIFEST.MF定義了以下Web-ContextPath屬性,
Web-ContextPath: /uas
那麼訪問這個WEB應用程序的URL總是相對於http://host:port/uas,需要注意的是: Web-ContextPath屬性的值總是以斜槓’/’開始。
當安裝WAB時,除非Web-ContextPath屬性出現在MANIFEST.MF中且Web-ContextPath的值是一個有效的值,否則,Web Extender會認為這不是一個WAB,而視為一個普通的Bundle。
WAB結構和相關的OSGi元數據
上面已經看到,除了標准OSGi元數據,WAB必須要在META-INF/MANIFEST.MF文件中定義Web-ContextPath屬性。例如,以下是一個WAB的結構,
圖2: 一個WAB的結構示例
這個WAB定義的OSGi元數據如下所示,
圖3:圖2的WAB的OSGi元數據示例
在圖2中,我們定義了一個WAB,這個WAB中有一個Servlet,被放在了WEB-INF/classes目錄下,而且這個WAB有兩個內部依賴,lib1.jar和lib2.jar。當安裝WAB時,為了使這些動態的內容都能夠被Web服務器訪問到,我們就必須在這個WAB的MANIFEST.MF中按照一定的規則指定OSGi元數據,也就是圖3所示的那樣,
指定一些必須的屬性包括Bundle-ManifestVersion、Bundle-SymbolicName、Bundle-Version。Bundle-Name是可選的,這是一個歷史遺留的屬性,你可以不用指定它,但是我通常也會指定這個屬性,因為,Bundle-Name屬性的值可以用來反映Bundle的用途。
指定Import-Package屬性,因為這個WAB正在使用Servlet,所以我們導入了Servlet相關的包。
指定Bundle-ClassPath屬性,這個屬性非常重要,它定義了如何加載內部的依賴和WAB自身的類,我們把WEB-INF/classes/放在WEB-INF/lib/lib1.jar和WEB-INF/lib/lib2.jar的前面,這樣做是為了和傳統WAR文件搜索類的順序一致,簡單地說,優先搜索WAB自身的Class,然後再搜索依賴的庫文件。
指定Web-ContextPath屬性。
通過對MANIFEST.MF追加OSGi元數據,也再次說明了WAB使用OSGi生命周期和類/資源加載規則而不是標准JavaEE環境的加載規則,這點至關重要。
WAB的生命周期
在圖1中已經提到了WAB的生命周期,仔細地與標准OSGi Bundle的生命周期比較一下,你會發現,WAB的生命周期多了四個階段(DEPLOYING、DEPLOYED、UNDEPLOYING和UNDEPLOYED)。
當一個WAB處於DEPLOYED階段時,它已經做好了准備來服務即將到來的WEB請求。處於DEPLOYED階段也意味著這個WAB或者處於ACTIVE狀態,或者處於STARTING狀態(因為有一個懶惰的激活策略)。關於懶惰的激活策略,在《OSGi In Action》一書第 9.3節“Starting bundles lazily”有精彩的介紹。
對於具有懶惰的激活策略的WAB來說,Web Extender應該確保當服務WEB的靜態內容(如圖像資源、HTML和CSS等)時不能改變該WAB所處的狀態,即仍然使它處於STARTING狀態。
從圖1中,我們能夠清楚地看到,為了讓WAB能夠服務即將到來的WEB請求,WAB需要從 DEPLOYING遷移到DEPLOYED階段,Web Extender必須部署WAB中的WEB應用程序相關的類和資源到Web運行時。具體地,
等待WAB處於ACTIVE狀態或STARTING狀態
發送org/osgi/service/web/DEPLOYING事件
驗證Web-ContextPath屬性的值沒有和其他已經被部署的WEB應用程序的上下文路徑沖突,也就是說保證上下文路徑的唯一性。如果有沖突,那麼部署WAB失敗,Web Extender應該記錄下部署失敗的日志。
如果3的驗證通過,那麼按照以下的順序,Web運行時開始處理部署相關的細節,如果web.xml存在的話,它也會處理web.xml中的內容。
為這個WEB應用程序創建一個Servlet上下文
初始化配置的Servlet事件偵聽器
初始化配置的應用程序過濾器等
注冊Servlet上下文作為OSGi服務
發送org/osgi/service/web/DEPLOYED事件通知當前的WAB已經准備好了,可以服務WEB請求。
如果在org/osgi/service/web/DEPLOYED事件發送前的任何時候有異常或錯誤發生,那麼WAB的部署將失敗。
圖1中我們也能夠發現,一旦不再需要該WAB服務Web請求時,那麼該WAB需要從DEPLOYED經過UNDEPLOYING遷移到UNDEPLOYED階段(UNDEPLOYING是一個暫態)。
有幾種方法能夠使WAB處於UNDEPLOYED階段,
方法1: 停止WAB
一旦接收到WAB STOPPING事件,Web Extender必須立刻從Web運行時中undeploy Web應用程序資源。Undeploy的主要步驟如下:
發送org/osgi/service/web/UNDEPLOYING事件通知Web應用程序資源將被undeploy。
從OSGi注冊表中移去Servlet上下文。
Web運行時必須讓該Web應用程序停止服務請求。
Web運行時必須清理所有Web應用程序相關的資源,如占用的JAR,以及清理ClassLoader避免內存洩漏等。
發送org/osgi/service/web/UNDEPLOYED事件。
方法2: 卸載(Uninstall)WAB
除了停止WAB,也能夠通過從OSGi運行時中卸載WAB來undeploy對應的Web應用程序資源,undeploy步驟和方法1一樣。
方法3:停止Web Extender
當停止Web Extender時,所有被部署的WAB都將被undeploy,但是,盡管WAB被undeploy了,它任然處於ACTIVE狀態。
從以上可以得出,WAB生命周期的四個特有狀態不同於標准OSGi Bundle的狀態,WAB生命周期的特有狀態並不受OSGi生命周期層控制,不是標准的OSGi狀態,這些特有的狀態僅僅由Web Extender控制。
關於WAB生命周期,在“深入剖析GlassFish OSGi/WEB容器”中將再次闡述。
另外,當你閱讀OSGi Enterprise Release 5 Specification時,特別要注意不能將Uninstall和Undeploy混為一談,盡管在一些場合下這兩個術語都能夠理解為“卸載”。
OSGi Web容器
最後我們來談一下OSGi Web容器,在上面的章節中我們已經多次提到了Web Extender,Web 運行時以及Web URL Handler。這些實體構成了OSGi Web容器,而OSGi Web容器是OSGi Web規范的實現。根據OSGi Web規范,OSGi Web容器由以下三個實體構成:
Web Extender
驗證是否為WAB並且跟蹤WAB的生命周期,同時負責部署WAB到Web運行時以及undeploy一個被部署的WAB。
Web運行時
Web應用程序運行時環境,對於GlassFish來說,Web運行時基於Tomcat Catalina。
Web URL Handler
一個URL Stream Handler,這個URL Stream Handler能夠處理webbundle: scheme,這個scheme能夠被用來轉換以及安裝WAR到OSGi運行時中,GlassFish 4提供了一個新的特性,即通過實現這個URL Stream Handler在部署時自動轉換和安裝WAR到OSGi運行時。
構建一個OSGi WEB應用程序
回到開始提出的問題場景,即如何在不停止JVM的情況下,構建一個動態的Web應用程序?
以下的Sample應用程序源於我曾經調查的一個GlassFish問題 [3],先看一下需求,
問題場景
我們希望構建這樣一個Web應用程序,當啟動這個Web應用程序時,沒有任何Web模塊被加載,界面顯示“No modules available.”,然後,當部署一個Web模塊後,在浏覽器上點擊刷新按鈕(不重啟應用程序),這個Web模塊隨即出現在界面上。
開發工具
在本文中我將使用如下的一些工具來構築開發環境,其中,根據個人的使用習慣,你也可能使用NetBeans或IntelliJ IDEA。
JavaSE7
Maven 3.0.4
Eclipse Kepler
應用程序架構
下面詳細地說明一下圖4,
將要創建的應用程序分成Web前端,存放模塊接口的Core,以及實現模塊接口的各個模塊。Web前端采用JSF 2+CDI,也就是使用JavaEE CDI Bean與JSF頁面進行綁定。應用程序的每個Web模塊都需要實現Core中的模塊接口。
Web前端以WAB方式打包,Core和每個模塊打包成標准OSGi Bundle。
每個模塊有一個OSGi Activator,一旦部署模塊到GlassFish OSGi運行時,將首先執行模塊的Activator方法來注冊模塊服務以便讓Web前端的服務偵聽器能夠獲取到相應的模塊服務。
一旦Web前端的服務偵聽器(ServiceListener)發現有新的模塊被注冊,那麼該模塊將被添加到應用程序Bean的模塊集合中,類似的,一旦發現既有的模塊從OSGi服務注冊表中刪除,那麼應用程序Bean的模塊集合將移除該模塊。
構築開發環境來創建應用程序
我們將使用Maven來一步一步地創建一個多模塊的工程,我推薦使用如下的方式來創建多模塊的工程,關於這種方式的詳細說明,你能夠參考[4]。
假設我使用Windows平台來創建Sample應用程序。
創建Sample應用程序的Parent Pom文件
運行Windows命令行,在當前的工作目錄下,執行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish.wab.sample -DarchetypeArtifactId=maven-archetype-site-simple
成功執行後,你會發現在當前工作目錄下創建了一個“glassfish.wab.sample“目錄,並且有一個pom.xml文件,這個文件就是Sample應用程序的Parent Pom文件。
配置Sample應用程序的Parent Pom文件
打開Sample應用程序的Parent Pom文件,放入以下的內容,
<build>
<finalName>${project.artifactId}</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<!-- 2.2.0 and above have new bnd which has wab
instruction. 2.3.4 has
few important bug fixes. -->
<version>2.3.4</version>
<extensions>true</extensions>
<configuration>
<supportedProjectTypes>
<supportedProjectType>ejb</supportedProjectType>
<supportedProjectType>war</supportedProjectType>
<supportedProjectType>bundle</supportedProjectType>
<supportedProjectType>jar</supportedProjectType>
</supportedProjectTypes>
<instructions>
<!-- Read all OSGi configuration info from this
optional file -->
<_include>-osgi.properties</_include>
<!-- No packages are exported by default. Having
any pattern is dangerous, as the
plugin will add any package found in
dependency chain that matches the pattern as well.
Since there is no easy way to have an
include filter for just local packages, we don't
export anything by default.-->
<Export-Package>!*</Export-Package>
</instructions>
</configuration>
…
</plugin>
…
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
<version>4.2.0</version>
<scope>provided</scope>
</dependency>
…
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Add the the following dependencies to every module to save
user from
adding them to every one. -->
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
</dependency>
…
</dependencies>
以上內容基於https://svn.java.net/svn/glassfish~svn/trunk/fighterfish/sample/parent-pom/pom.xml ,完整的POM文件內容,請參照https://github.com/tangyong/GlassFishOSGiJavaEESample/blob/master/glassfish.wab.sample
/pom.xml。
你一定會問,為什麼要放入這些內容?以下是幾個重要的原因:
Maven工程的POM文件有很好的繼承關系,就像面向對象的類設計一樣,將子工程需要的一些共通插件(plugin)和共通的依賴(dependency)放入到Parent POM文件中總是很好的做法。
為了構建WAB,我們放入maven-bundle-plugin[5],maven-war-plugin[6]以及為了編譯Java源文件所需要的maven-compiler-plugin[7]等。這裡,需要說一下maven-bundle-plugin,這個插件的目的是將工程打包成標准OSGi Bundle的文件格式,其內部使用了bnd[8],bnd是由OSGi聯盟前主席Peter Kriens創建,用來簡化開發OSGi Bundle的痛苦。從上面的maven-bundle-plugin的配置看,有一個地方需要特別說明:
<instructions>
<_include>-osgi.properties</_include>
<Export-Package>!*</Export-Package>
</instructions>
上述的指令中,通過“_include”標簽指定了一個配置OSGi元數據的文本文件,這個文本文件的位置相對於當前Maven工程的根目錄(你也可以自行配置它的位置),osgi.properties中的內容是一組指定OSGi元數據的規則,以下是一個osgi.properties的示例:
Export-Package: \
sample.foo; \
sample.bar; version=${project.version}
Import-Package: \
sample.car;resolution:=optional, \
*
Bundle-SymbolicName: \
${project.groupId}.${project.artifactId}
…
關於詳細的指定規則,請參見[9]。
這裡也要特別說明一下,我們使用Maven War插件的2.4版本而不是2.1版本,因為2.1版本在Windows平台上打包時,會生成兩個web.xml文件。這個問題同樣出現在fighterfish子工程的Sample Parent POM中,我將很快修復它。
Export-Package
在上面的maven-bundle-plugin的配置中,還出現了<Export-Package>!*</Export-Package>,這個標簽以及標簽值的含義是,默認地,這個OSGi Bundle不導出任何包,除非我們顯示地在osgi.properties中指定“Export-Package”值。
創建Core子工程
從Windows命令行進入“assfish.wab.sample“目錄,執行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish.wab.sample.core
成功執行後,你會發現在“glassfish.wab.sample“目錄下創建了一個“glassfish.wab.sample.core“目錄,進入“glassfish.wab.sample.core“目錄並打開pom.xml文件,你會發現以下內容已經自動被添加了。
<parent>
<groupId>cn.fujitsu.com.tangyong</groupId>
<artifactId>glassfish.wab.sample</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
然後,在“glassfish.wab.sample.core“目錄下創建一個osgi.properties文件,內容如下:
Export-Package={local-packages}; version=${project.version}
這樣的話,當構建最終Bundle時,Bundle將導出內部的帶有工程版本的包。
創建Web客戶端子工程
類似3,執行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish
.wab.sample.web -DarchetypeArtifactId=maven-archetype-webapp
成功執行後,你會發現在“glassfish.wab.sample“目錄下創建了一個“glassfish.wab.sample.web“目錄。然後,新建src/main/java和src/main/resources/META-INF目錄。默認地,這兩個目錄不會被創建。
接著,在“glassfish.wab.sample.web“目錄下創建一個osgi.properties文件,內容如下:
Web-ContextPath:/wabsample
我指定了這個WAB的Web上下文路徑為/wabsample,你也可以自行修改為其他的值。
創建WEB模塊1子工程
類似4,執行以下命令:
mvn archetype:create -DgroupId=cn.fujitsu.com.tangyong -DartifactId=glassfish
.wab.sample.module1 -DarchetypeArtifactId=maven-archetype-webapp
成功執行後,你會發現在“glassfish.wab.sample“目錄下創建了一個“glassfish.wab.sample.module1“目錄。
然後,打開該工程的pom文件,添加“glassfish.wab.sample.core“依賴聲明,
<dependency>
<groupId>cn.fujitsu.com.tangyong</groupId>
<artifactId>glassfish.wab.sample.core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
創建WEB模塊2子工程
類似5,這裡就跳過。
設置開發環境
一旦這些Maven子工程工程創建成功,我們將進行開發環境的設置,進入編碼階段,以下的步驟描述了如何將Maven集成到Eclipse。假定我的Eclipse工作空間(Workspace)是“E:\QCON\WS“。
修改Kepler中的M2_REPO變量
修改Kepler中的M2_REPO變量的目的是為了設置M2_REPO的值為你機器上的Maven本地倉庫(Local Repository)。默認地,Kepler中的M2_REPO變量的值為~/.m2/repository。具體的修改步驟可以參照[10]。
為Maven工程創建Eclipse相關的文件(如,.project文件)
從Windows命令行進入“glassfish.wab.sample“目錄,執行以下命令:
mvn eclipse:eclipse
然後將“glassfish.wab.sample“工程連同子工程導入到Eclipse中。如果一切成功的話,在Eclipse中,應該看到類似如下的畫面。
圖5: 成功導入到Eclipse的Sample應用程序結構示意圖
應用程序核心邏輯
glassfish.wab.sample.core
新建一個名為“Module“的接口, 該接口的定義如下:
public interface Module {
public String getModuleName();
public URL getResource(String path);
public String getAboutPage();
}
glassfish.wab.sample.web
Web子工程的核心是ApplicationBean類,這也是一個CDI Bean,並且和JSF頁面綁定在一起,成為了JSF托管Bean(Managed Bean)。以下是home.xhtml頁面中與ApplicationBean相關的內容,
<h:body>
<h:panelGroup layout="block" rendered="#{not empty applicationBean.modules}">
Modules:
<br />
<ui:repeat value="#{applicationBean.modules}" var="module">
<h:panelGrid columns="1">
<h:link outcome="#{module.moduleName}#{module.aboutPage}" value="#
{module.moduleName}" />
</h:panelGrid>
</ui:repeat>
</h:panelGroup>
<h:panelGroup layout="block" rendered="#{empty applicationBean.modules}">
No modules available.
</h:panelGroup>
</h:body>
其中,#{applicationBean.modules}是JSF表達式語言,通過這個表達式,能夠獲取到ApplicationBean類實例中的modules變量的值。在設計這個頁面時,我們通過<ui:repeat>標簽動態地追加<h:panelGrid>,一旦有新的模塊到來或者既有模塊被移除,ApplicationBean類實例中的modules變量的值將發生改變,然後,當刷新浏覽器時,JSF頁面將呈現出不同的內容。
那麼,ApplicationBean類實例是如何跟蹤到模塊的注冊和移除的呢?首先,讓我們看一下ApplicationBean類的定義:
@Named
@ApplicationScoped
public class ApplicationBean {
@Inject
private BundleContext bc;
@Inject
private ModuleListener moduleListener;
private List<Module> modules = new ArrayList<Module>();
@Inject
public void initialize(ServiceTracker st) {
bc.addServiceListener(moduleListener);
loadServices(st);
}
public void afterAddModule(Module module) {
System.out.println("Module added.");
modules.add(module);
}
public void beforeRemoveModule(Module module) {
System.out.println("Module removed");
modules.remove(module);
}
public List<Module> getModules() {
return modules;
}
private void loadServices(ServiceTracker st) {
ServiceReference[] srs = st.getServiceReferences();
if (srs != null) {
for (ServiceReference sr : srs) {
Module m = (Module) bc.getService(sr);
modules.add(m);
}
}
}
}
以上定義中,moduleListener扮演了重要的作用,moduleListener是org.osgi.framework.ServiceListener的一個實現,ServiceListener的作用是用來跟蹤OSGi服務的注冊,更新以及卸載。afterAddModule 和beforeRemoveModule作為回調方法被moduleListener調用,具體地,moduleListener中注入了ApplicationBean實例,一旦有新的模塊到來,moduleListener就會通過ApplicationBean實例來調用afterAddModule方法,如果既有的模塊被移除,那麼就調用beforeRemoveModule方法。
在glassfish.wab.sample.web中還有一些其他的類,因為篇幅關系,就不一一敘述了,詳細地內容請參見: https://github.com/tangyong/GlassFishOSGiJavaEESample/tree/master
/glassfish.wab.sample/glassfish.wab.sample.web
glassfish.wab.sample.module1
模塊1很簡單,只有兩個類,實現Module接口的Module1類和BundleActivator的實現類Activator。我們必須要追加一個BundleActivator的實現類以便模塊1在啟動時能夠將自己注冊到GlassFish OSGi運行時的服務注冊表中。
詳細的內容請參見: https://github.com/tangyong/GlassFishOSGiJavaEESample/tree/master
/glassfish.wab.sample/glassfish.wab.sample.module1
glassfish.wab.sample.module2
類似於模塊1,這裡就省略跳過。
完整的Sample應用程序,請從https://github.com/tangyong/GlassFishOSGiJavaEESample 中下載。
使用GlassFish 4部署OSGi WEB應用程序
一旦我們構建完Sample應用程序,就將使用GlassFish 4來部署它。
安裝和啟動GlassFish
首先,你需要從以下鏈接下載一個GlassFish 4的安裝zip包。然後解壓縮這個zip包到本地的文件系統。
http://download.java.net/glassfish/4.0/release/glassfish-4.0.zip
然後,通過以下命令,啟動GlassFish的domain,默認地,GlassFish會為你創建好一個domain。假設解壓縮後的GlassFish所在的目錄用$GlassFish_HOME表示,
cd $GlassFish_HOME/glassfish4/glassfish/bin
asadmin start-domain
更多的關於GlassFish 4的文檔,請參考: http://glassfish.java.net/documentation.html
部署OSGi應用程序的方式
基本上,使用GlassFish 4部署OSGi應用程序有三種方式,
使用asadmin deploy命令
在命令行或者Shell中,使用類似如下的命令,
asadmin deploy –type=osgi XXX.jar或XXX.war
當部署WAB時,經常容易遺漏—type=osgi,如果遺漏這個選項,那麼你所做的就是在部署一個標准的WAR而不是WAB。
使用autodeploy的方式
這是一個非常快捷的部署方式,你只需要將要部署的Bundle放到$GlassFish_HOME/glassfish4/glassfish/domains/domain1/autodeploy/bundles目錄下就可以了。這種方式是將Apache Felix File Install[11]集成到GlassFish中,使用這種方式甚至能夠考慮Bundle之間的依賴。詳細地內容,請看一下[12]。
使用asadmin osgi命令
GlassFish 3允許你通過telnet登陸到GlassFish OSGi運行時的後台,然後通過以下的方式來安裝並啟動一個WAB,
install webbundle:file:///tmp/mybundle.war
start <bundle_id>
但是,到了GlassFish 4,這種telnet的方式已經被禁止了,原因是telnet的方式並不安全,因此,GlassFish 4提供了一種新的方式去直接操作OSGi運行時,即通過執行asadmin osgi ...命令,例如,上面的命令等同於以下,
asadmin osgi install file:///tmp/mybundle.war
asadmin osgi start <bundle_id>
對於asadmin osgi命令,最常用的就是,當你部署完一個OSGi Bundle或者想看一下某些Bundle的Id或者當前狀態時,使用asadmin osgi lb命令能夠列舉出OSGi運行時中所有的Bundle。
對於這三種方式,我更加傾向於使用“使用autodeploy的方式“,因為它更加簡單,更有效率。對於“使用asadmin deploy命令”,絕大多數場合,執行的效率也很好,但是,當你的程序使用vaadin時,部署將會非常慢,這是GlassFish需要急需改進的一個特性,相信很快將會得到改善。
部署並運行Sample應用程序
現在,我們可以按照如下的順序部署並運行Sample應用程序了,
部署glassfish.wab.sample.core
執行“asadmin deploy –type=osgi glassfish.wab.sample.core.jar”
部署glassfish.wab.sample.web.war
執行“asadmin deploy –type=osgi glassfish.wab.sample.web.war“
在浏覽器上鍵入“http://localhost:8080/wabsample/“,應該沒有出現任何模塊,如下圖所示,
部署glassfish.wab.sample.module1和glassfish.wab.sample.module2
執行“asadmin deploy –type=osgi glassfish.wab.sample.module1.war“ 以及”asadmin deploy –type=osgi glassfish.wab.sample.module2.war“
在浏覽器上點擊刷新按鈕,此時,模塊1和模塊2都出現了,如下圖所示,
然後,再執行“asadmin osgi lb“命令看一下剛剛我們部署的Bundle的狀態,
執行以下命令卸載模塊2
“asadmin undeploy glassfish.wab.sample.module2“
然後,在浏覽器上再次點擊刷新按鈕,此時,模塊2已經消失了,如下圖所示,
剖析GlassFish OSGi/WEB容器
到這裡為止,如果你仔細閱讀上面的內容,我想你應該已經掌握了如何開發和部署一個WAB,並且也應該理解了WAB和標准OSGi Bundle以及和標准WAR的區別。讓我們再深入一下,看看GlassFish是如何實現OSGi WEB應用程序規范的。
混合應用程序Bundle(Hybrid Application Bundle)
從GlassFish的角度看,WAB又是混合應用程序Bundle的一種類型。混合應用程序Bundle既是一個標准OSGi Bundle,又是一個JavaEE模塊。在運行時,它既有一個OSGi Bundle上下文,又有一個JavaEE上下文。目前,GlassFish支持兩種類型的混合應用程序Bundle,Web應用程序Bundle和EJB應用程序Bundle。關於EJB應用程序Bundle,我將放在Part3中。
當一個混合應用程序Bundle被部署到GlassFish OSGi運行時,GlassFish能夠觀察到它的生命周期,使用熟知的“Extender模式[13]“,將Bundle中的一些部分部署或Undeploy到JavaEE容器中。混合應用程序Bundle的生命周期如下所示,
圖6: 混合應用程序Bundle的生命周期
摘自: “OSGi Application Development using GlassFish Server“
如果你仔細看一下圖6和圖1,本質上兩幅圖是一樣的,圖6並沒有在OSGi生命周期的基本狀態上增加4個部署和Undeploy相關的狀態,但是,圖1中的4個狀態所涉及的操作都反映到了圖6中。
GlassFish OSGi Web容器的實現
GlassFish OSGi Web容器實現了OSGi Web應用程序規范。通過部署WAB,我們能夠清晰地理解GlassFish部署OSGi Web應用程序的流程以及如何實現規范的。部署流程分為兩個階段,
和部署標准OSGi Bundle一樣,部署WAB到OSGi運行時中。
當WAB的生命周期變為ACTIVE狀態或者STARTING狀態(因為有一個懶惰的激活策略)時,部署該WAB到JavaEE運行時中。
需要注意的是,1和2是異步的,這與Undeploy過程不同,Undeploy是同步的,也就是說,一旦該WAB被停止或卸載,將立即從JavaEE運行時中Undeploy該WAB,並且清理相應的資源。
以下,我將使用“asadmin deploy命令”來剖析部署的流程。
【階段1】部署WAB到OSGi運行時
階段1的部署主要包括兩個部分: a.安裝WAB到OSGi運行時 b.啟動該WAB使其處於ACTIVE狀態或者STARTING狀態。
以下是部署WAB到OSGi運行時的時序圖,
圖7: 部署WAB到OSGi運行時的時序圖
根據部署的類型,ApplicationLifecycle類獲取相應的AchiveHandler。因為我們正在部署WAB,當執行“asadmin deploy“命令時,我們傳遞了“—type=osgi”,因此,部署的類型為osgi。獲取到的AchiveHandler是OSGiArchiveHandler。AchiveHandler負責處理和訪問某種特定檔案中的資源,這些檔案包括了WAR,JAR,RAR以及Bundle。AchiveHandler將在構建部署ClassLoader,獲取Sniffer等後續動作中被使用到。
另外,ApplicationLifecycle類是部署的核心類,也是部署命令核心邏輯執行的入口點,從它的所在的位置能夠看出它的重要性,它位於GlassFish內核模塊。
接下來,ApplicationLifecycle類通過SnifferManagerImpl類獲取相應的Sniffer。那麼,什麼是Sniffer呢?自從GlassFish v3開始,根據部署的請求,Sniffer被用來分析和選擇合適的容器來處理應用程序的類型。分析和選擇的過程可能簡單,也可能復雜。例如,通過查找WEB-INF/web.xml或者是否以.war結尾來分析是否需要WEB容器,也可能通過注解(Annotation)掃描來判斷是否需要EJB容器。對於WAB的情形,SnifferManagerImpl返回了OSGiSniffer。進一步地,Sniffer接口有個重要的方法叫“getContainersNames”,對於OSGiSniffer,這個方法返回“osgi”。這個方法將被用來獲取相應的容器。
有了具體的Sniffer之後,ApplicationLifecycle類通過ContainerRegistry類的getContainer(String containerType)方法來獲取相應的容器,其中,containerType就是2)中提到的“getContainersNames”的返回值。進一步地,getContainer(String containerType)方法返回了一個EngineInfo對象,這個對象擁有特定容器的信息。對於WAB情形,這個特定的容器是OSGiContainer。以下是一個調試的信息,給出了EngineInfo對象中的內容。
其中,你可以發現container的類型是一個ServiceHandleImp,這是一個HK2相關的類,以下是OSGiContainer的代碼,
@Service(name = OSGiSniffer.CONTAINER_NAME)
@Singleton
public class OSGiContainer implements Container {
public Class<? extends Deployer> getDeployer() {
return OSGiDeployer.class;
}
public String getName() {
return OSGiSniffer.CONTAINER_NAME; // used for reporting
purpose,so any string is fine actually
}
}
關於HK2的內容,我將在Part7中詳細闡述。這裡簡單地說一下,首先,HK2是一個JSR330的實現。其次,OSGiContainer使用@Service來標注這個類是一個HK2的服務,並且用name屬性來方便HK2進行依賴注入。另外,使用@Singleton來標注當使用HK2獲取OSGiContainer實例時,使用Singleton的方式。再者,這個類中最為重要的方法是getDeployer,該方法返回了OSGiDeployer,用於後續的OSGi部署。
從以上的定義能夠看出,OSGiContainer的實例由HK2負責創建並不是通過new出來的,因此,EngineInfo對象中的內容很自然地變成了ServiceHandleImp。
接下來就是通過EngineInfo對象獲取相應的Deployer了,Deployer真正負責部署{3)中我們已經知道對於WAB情形,EngineInfo將返回OSGiDeployer。
然後,ApplicationLifecycle類委托OSGiDeployer類來安裝WAB到OSGi運行時中,OSGiDeployer進而使用BundleContext來安裝該WAB。安裝成功後,該WAB的狀態將變為INSTALLED。
當安裝成功後,ApplicationLifecycle類開始委托OSGiDeployedBundle類來啟動該WAB,當然,在啟動之前,需要首先判斷該Bundle不是一個Fragment,然後再通過Bundle.start方法來啟動該WAB。
上面提到的Sniffer等概念,在GlassFish Wiki[14]中有更為詳細地說明。
【階段2】部署WAB到JavaEE運行時
在闡述階段2之前,需要先回到GlassFish Domain的啟動,這部分內容將在Part8中詳細地說明。也許你會問,為什麼要回到GlassFish Domain的啟動?
原因在於從階段1過渡到階段2,需要做一些必要的工作,例如: 在“WAB生命周期”一章中,提到過為了部署WAB到JavaEE運行時,前提條件是等待WAB處於ACTIVE狀態或STARTING狀態,那麼如何等待?在OSGi開發中,一個常見的模式是使用BundleTracker類來跟蹤已被安裝的Bundle的生命周期變化。通常,打開BundleTracker的操作是由OSGi Activator完成的,而OSGi Activator(如果有的話)是啟動OSGi Bundle最先執行的方法,因此,必須有一個Bundle做這樣的BootStrap動作。GlassFish OSGi-JavaEE遵循了這一設計模式,所以,為了搞清楚哪些Bundle在完成這些BootStrap動作,我們必須回到GlassFish Domain的啟動。
GlassFish安裝目錄下有個目錄叫glassfish4/glassfish/modules/autostart,這裡放置了一些Bundle,其中,有兩個Bundle與本文密切相關: 1) osgi-javaee-base.jar 2) osgi-web-container.jar。
首先,看一下它們的作用,osgi-javaee-base是GlassFish OSGi-JavaEE實現的基類,主要使用了Extender模式來構建整個OSGi-JavaEE的框架,是GlassFish OSGi-JavaEE實現的靈魂。osgi-web-container實現了OSGi Web規范,也是本文重點要剖析的對象。
其次,osgi-javaee-base和osgi-web-container都定義了Activator,當啟動GlassFish Domain後,osgi-javaee-base.jar和osgi-web-container.jar被部署到GlassFish OSGi運行時中,且這兩個Bundle都被激活處於Active狀態,在到達Active狀態之前,各自的Activator都被調用。讓我們來看看它們的Activator都做了什麼。
osgi-javaee-base的Activator
osgi-javaee-base的Activator叫“OSGiJavaEEActivator”,它的start方法中核心的邏輯是啟動ExtenderManager,以及注冊並啟動JavaEEExtender。ExtenderManager的作用是負責啟動任何已經被注冊的Extender服務。以下是相應的代碼,
private synchronized void startExtenders() {
//Because of a race condition,we can be started multiple times, so
check if already started
if (extenderTracker != null) return;
// open will call addingService for each existing extender
// and there by we will start each extender.
extenderTracker = new ExtenderTracker(context);
extenderTracker.open();
}
可以清楚地看到,啟動的邏輯主要在ExtenderTracker中,讓我們看一下
private class ExtenderTracker extends ServiceTracker {
ExtenderTracker(BundleContext context)
{
super(context, Extender.class.getName(), null);
}
@Override
public Object addingService(ServiceReference reference)
{
Extender e = Extender.class.cast(context.getService
(reference));
logger.logp(Level.FINE, "ExtenderManager$ExtenderTracker","
addingService",
"Starting extender called {0}", new Object[]{e});
e.start();
return e;
}
…
ExtenderTracker是一個ServiceTracker,在OSGi開發中,使用ServiceTracker來跟蹤注冊的OSGi服務已經成為了經典的模式。這裡,ExtenderTracker跟蹤的服務類型是Extender接口。一旦某個Extender被注冊,那麼ExtenderTracker將調用addingService方法然後啟動這個Extender。
前面提到,除了啟動ExtenderManager,osgi-javaee-base也注冊並啟動JavaEEExtender,這個JavaEEExtender非常重要,它的作用就是負責偵聽和部署混合應用程序Bundle。看一下它的start方法,
public synchronized void start() {
executorService = Executors.newSingleThreadExecutor();
c = new OSGiContainer(context);
c.init();
reg = context.registerService(OSGiContainer.class.getName(),
c, null);
tracker = new BundleTracker(context, Bundle.ACTIVE | Bundle.
STARTING, new HybridBundleTrackerCustomizer());
tracker.open();
}
其中,最重要的是初期化並注冊OSGiContainer以及打開一個BundleTracker來跟蹤混合應用程序Bundle是否處於Active或Starting狀態。對於OSGiContainer,它具體負責了部署的過程,搭建了部署的骨架。對於BundleTracker來說,它回答了早期提到的“如何等待WAB處於ACTIVE狀態或STARTING狀態”的問題。對於HybridBundleTrackerCustomizer類,其中的addingBundle方法值得我們看一下,
public Object addingBundle(final Bundle bundle, BundleEvent event) {
if (!isStarted()) return null;
final int state = bundle.getState();
if (isReady(event, state)) {
Future<OSGiApplicationInfo> future = executorService.
submit(new Callable<OSGiApplicationInfo>() {
@Override
public OSGiApplicationInfo call()throws Exception{
return deploy(bundle);
}
});
deploymentTasks.put(bundle.getBundleId(), future);
return bundle;
}
return null;
}
可以清晰地看到,一旦混合應用程序Bundle處於Active或Starting狀態,那麼,立刻啟動一個線程進行部署。
osgi-web-container的Activator
osgi-web-container的Activator是OSGiWebContainerActivator,這個類的start方法很簡單,注冊WebExtender作為OSGi服務。可以看出,osgi-web-container遵循了Extender模式,一旦注冊成功,osgi-javaee-base中的ExtenderTracker將跟蹤到它並調用它的start方法。下圖是WebExtender的主要處理邏輯,
階段2的前傳已經講完,接下來,回到階段2的部署上來,以下是階段2中主要的部署時序圖,
圖9: 階段2中主要的部署時序圖
下面,詳細地說明一下圖9中的各個時序動作,
當JavaEEExtender中的HybridBundleTrackerCustomizer跟蹤到WAB處於Active或Starting狀態時,開始調用OSGiContainer的deploy方法,這裡的OSGiContainer來自osgi-javaee-base模塊並不是階段1中提到的OSGiContainer,請注意區分。
OSGiContainer的deploy方法首先選擇正確的Deployer,方法是通過遍歷所有已經注冊的OSGiDeployer服務,然後逐個調用這些OSGiDeployer服務的handles方法來選擇正確的Deployer。對於WAB情形,正確的Deployer是OSGiWebDeployer,它的handles方法如下:
final Dictionary headers = b.getHeaders();
return headers.get(Constants.WEB_CONTEXT_PATH) != null &&
headers.get(org.osgi.framework.Constants.FRAGMENT_HOST)
== null;
很清晰地看到,如果當前Bundle的元數據中包含了Web-ContextPath且不包含 Fragment-Host,那麼該Bundle是一個WAB,且OSGiWebDeployer能夠處理這種類型的混合應用程序Bundle。
選擇完正確的Deployer後,OSGiContainer委托OSGiWebDeployer執行具體的部署。OSGiWebDeployer的deploy方法首先創建OSGi部署請求(OSGiWebDeploymentRequest),然後調用OSGiWebDeploymentRequest的execute方法進行部署,在execute方法中,主要執行預處理(preDeploy),部署的准備工作(prepare),實際的部署(deploy),以及後處理(postDeploy)。
預處理的核心邏輯是使用ContextPathCollisionDetector檢測Web上下文路徑沖突,這是OSGi Web規范必須的。
部署的准備工作中最重要的是創建一個OSGiWebDeploymentContext,OSGiWebDeploymentContext是GlassFish WAB支持的心髒,它負責為WAB創建一個類加載器(class loader)以便當有Web請求時,Web容器能夠正確地加載到WAB中相關的靜態資源和動態資源。這個類加載器為WABClassLoader,這個類加載器繼承了org.glassfish.web.loader.WebappClassLoader,而後者專門是GlassFish Web容器用來實現資源加載的。為了創建這個類加載器,需要重載OSGiDeploymentContext.setupClassLoader方法,如下所示:
protected void setupClassLoader() throws Exception {
finalClassLoader = new WABClassLoader(null);
shareableTempClassLoader = finalClassLoader;
WebappClassLoader.class.cast(finalClassLoader).start();
}
准備工作做完後,開始執行實際的部署,你可能已經發現,實際的部署再次委托到了階段1中提到的ApplicationLifecycle,是的,我們不需要再次發明輪子,因為ApplicationLifecycle在那裡,通過它,將這個WAB展開到glassfish4/glassfish/domains/domain1/applications中,在domain.xml中寫入部署的信息等,總之,像部署標准WAR一樣去部署它。
最後,還是要執行一下後處理工作,因為,一旦前面的動作部署失敗了,總是需要進行回滾使系統的狀態恢復到之前。
至此,WAB的部署以及相關的實現邏輯已經寫完了,詳細的代碼可以使用SVN下載GlassFish FighterFish子工程(https://svn.java.net/svn/glassfish~svn/trunk/fighterfish)來研究一下。
最後,想簡單地說一下對於未來的一些思考。
思考
制作新的Maven Archetype
在Part1寫完後,我看到有朋友在評論部分提到了OSGi中看不中用,從本文的WAB實例的構建看,確實也有不便之處。對於一些人為需要手動進行配置的部分(如pom文件),最好能夠盡可能的自動化。這項工作已經開始了!我和我的同事程曉明(@程_曉明)以及GlassFish FighterFish子工程的leader(Sahoo)正在制作新的Maven Archetype以便自動化一些繁瑣的構建工作,應該很快就會面世。
關於Sample的實用性
本文的Sample只是作為演示用,距離真正的實用性還有不小的差距,尤其是需要解決一個Bundle之間共享CDI BeanManager的問題,例如,讓模塊1中也能夠使用JSF托管的Bean,然後這並不是一件容易的事情,這需要在Web模塊和模塊1中架起一座橋梁,以便Web模塊中的BeanManager能夠發現模塊1中的CDI Bean。這個問題目前正在和JSF以及CDI的leader進行討論,期待能夠盡快解決。