前一篇文章,《模塊化Java: 動態模塊化》描述了如何通過使用服務 (service)給應用程序帶來動態模塊化特性。它們是通過輸出的一個(或多個 )可以在運行時被動態發現的接口而實現的。盡管這種方式使得client和server 完全解耦,但是又帶來一個如何(何時)啟動服務的問題。
啟動順序
在徹頭徹尾的動態系統裡,服務不僅可以在系統運行的時候裝卸,還可以以 不同的順序啟動。有時,這是個大問題:無論A和B的啟動順序如何,在系統達到 就緒狀態並准備好接收事件之前,如果沒有事件(或線程)出現,那麼哪個服務 先啟動都無大礙。
可是,有很多情況都不符合這一簡單假設。經典的例子就是logging: 通常, 服務在啟動和做其他操作的時候,就要連接並開始寫日志了。如果日志服務此時 還不可用,那會有什麼後果?
假定服務在運行時能夠動態裝卸,client應該能夠應對服務不存在時的情況 。在這種情況下,它也許能聰明地轉移到另一種機制(如輸出到標准輸出),或 者處於阻塞狀態等待服務可用(對logging系統來說不是好的答案)。可是,讓 服務啟動之前就可用是不切實際的。
啟動級別
OSGi提供了一種機制來控制bundle啟動時的順序,即使用啟動級別(start levels)。這一概念是基於UNIX運行級別的概念:系統以級別1啟動,然後單調 遞增,直到達到目標啟動級別。每個OSGi容器都提供了不同的默認目標級別: Equinox默認值是6;而Felix是1。
啟動級別可被用來創建bundle間的啟動順序,讓關鍵bundle服務(比如 logging)的啟動級別比那些需要用它的bundle更低。可是因為可 用的啟動級別 值是有限的,而且安裝程序傾向於選擇單一數字作為啟動級別,因此它並不能確 保你僅通過啟動順序就能解決問題。
另一點值得注意的是,具有相同啟動級別的bundle是各自獨立啟動的(可能 並行),因此,如果你有一個與log服務具有相同啟動級別的bundle,誰也不能 保證log服務能夠在需要的時候已經就緒。換句話說,啟動級別可以解決大部分 問題,但不能解決所有問題。
聲明式服務
解決這一問題的一個方案是OSGi的聲明式服務(以下稱為DS——declarative services)。用這一方法,各個組件是由外部bundle將他們組織在一起並決定他 們什麼時候可用。聲明式服務是通過在一個XML配置文件組織在一起的,文件中 描述了需要(消費)或提供什麼服務。
在上篇文章最後一個例子中,我們使用ServiceTracker去獲得服務,如果必 要則需等待服務可用。如果我們把創建shorten命令延遲到shortening服務可用 之後會很有用。
DS定義了一個組件(component)概念,其是比bundle更細粒度的概念,但是 比服務的概念粒度更大一些(因為一個組件可以消費/提供多個服務)。每個組 件都有一個名字,對應一個Java類,並可以通過調用該類的方法使其激活或失效 。與OSGi Java API不同,DS允許用純Java POJO來開發組件,根本不需要從程序 上依賴OSGi。其附帶的好處是讓DS更加易於測試和模擬(test/mock)。
為了說明這一方法,我們將繼續使用前面的例子。我們需要兩個組件:一個 是shortening服務本身,另一個是調用它的ShortenComand。
第一項任務是用DS配置並注冊shorten服務。我們可以讓DS在服務啟動時注冊 它,而不是通過Bundle-Activator注冊該服務。
那麼DS怎麼知道要激活並連接誰呢?我們需要給Bundle的Manifest頭增加一 個條目,其指示了一個(或多個)XML組件定義文件。
Bundle-ManifestVersion: 2
...
Service-Component: OSGI-INF/shorten-tinyurl.xml [, ...]*
這個 OSGI-INF/shorten-tinyurl.xml組件定義文件內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component name="shorten-tinyurl" xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0">
<implementation class="com.infoq.shorten.tinyurl.TinyURL"/>
<service>
<provide interface="com.infoq.shorten.IShorten"/>
</service>
</scr:component>
當DS處理這一組件時,其效果與代碼context.registerService( com.infoq.shorten.IShorten.class.getName(), new com.infoq.shorten.tinyurl.TinyURL(), null );基本一樣。Trim()服務需要類 似的聲明,在下面的源代碼中包含著這部分內容。
如果需要的話,一個單一組件可以基於不同接口提供多個服務。一個bundle 也可以包含多個組件,使用相同或不同的類,每個都提供不同的服務。
消費服務
要消費該服務,我們需要修改ShortenCommand,這樣它就綁定到IShorten服 務的一個實例上:
package com.infoq.shorten.command;
import java.io.IOException;
import com.infoq.shorten.IShorten;
public class ShortenCommand {
private IShorten shorten;
protected String shorten(String url) throws IllegalArgumentException, IOException {
return shorten.shorten(url);
}
public synchronized void setShorten(IShorten shorten) {
this.shorten = shorten;
}
public synchronized void unsetShorten(IShorten shorten) {
if(this.shorten == shorten)
this.shorten = null;
}
}
class EquinoxShortenCommand extends ShortenCommand {...}
class FelixShortenCommand extends ShortenCommand {...}
注意,不像上一次,這次沒有對OSGi API產生依賴;mock一個實現來檢驗其 是否工作正常也很輕松。那個synchronized修飾符確保了在服務get/set時不會 產生競爭情況。
為了告訴DS需要把IShorten服務實例綁定到我們的EquinoxShortenCommand組 件上,我們需要定義其所需的服務。當DS實例化你 的組件時(用默認構造器) ,它將通過調用定義在bind屬性裡的方法(setShorten())來設置IShorten服務 。
<?xml version="1.0" encoding="UTF-8"?>
<scr:component name="shorten-command-equinox" xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0">
<implementation class="com.infoq.shorten.command.EquinoxShortenCommand"/>
<reference
interface="com.infoq.shorten.IShorten"
bind="setShorten"
unbind="unsetShorten"
policy="dynamic"
cardinality="1..1"
/>
<service>
<provide interface="org.eclipse.osgi.framework.console.CommandProvider"/>
</service>
</scr:component>
無論bundle的啟動順序如何,一旦IShorten服務可用,該組件就將被實例化 並連接到這個服務。有關策略(policy)、基數性(cardinality)和服務 (service)的內容在下一節再做解釋。
策略和基數性
策略(policy)可被設為static或dynamic。static策略表示一旦設置,服務 不會變化。如果服務不可用了,組件也就失效了;如果一個新服務出現,那麼就 創建一個新的實例,並將該服務重新綁定。這顯然比我們就地更新服務要費勁得 多。
使用dynamic策略,當IShorten服務改變時,DS將對新服務調用setShorten() ,隨後對老服務調用unsetShorten()。
DS在unset之前調用set的原因是維持服務持續性。如果替換服務時先調用 unset,shorten服務就有可能短暫為null。這也就是為什麼unset方法還帶個參 數,而不是把服務設置為null的原因。
服務的基數性(cardinality)默認為1..1,其可取下列值之一:
0..1 可選的,最多1個
1..1 強制的,最多1個
0..n 可選的,多個
1..n 強制的,多個
如果不滿足基數性(例如,設置為強制,但是沒用shortening服務),那麼 組件是失效的。如果需要多個服務,那麼每個服務都調用一次setShorten()。相 反,對每個要卸載的服務都要調用unsetShorten()。
這裡並沒有展示組件在進入運行狀態時對每個實例進行定制的能力。
在DS 1.1裡,組件元素也有activate和deactivate屬性,在組件激活(啟動 )和失效(停止)過程中相應方法被調用。
最後,這一組件還提供一個CommandProvider服務的實例。這是一個Equinox 特定的服務,允許提供控制台命令,而這以前是在bundle的Activator中實現的 。這種模式的好處是,只要依賴服務可用,CommandProvider服務將自動被發布 ;除此之外,代碼本身不需要依賴任何OSGi API。
還需要針對Felix特定實現采用類似解決方案;因為到目前為止,OSGi command shell還沒有標准。OSGi RFC 147是一個正在進行中的規范,允許命令 在不同控制台執行。我們的例子源代碼中包含了shorten-command-felix組件的 完整定義。
啟動服務
上面所述方法讓我們可以以任何順序供給(及消費)shortening服務。一旦 command服務啟動了,它將綁定到可用的最高優先級的 shortening服務上;或者 ,如果沒有指定優先級,則綁定到擁有最低服務級別的服務上。我們現在不去考 慮次高優先級服務隨後是否應該被啟動,而是繼續使用目前已綁定到的服務。可 是,如果服務卸載,我們就要重新綁定,以維持最高優先級shortening服務對 client不會中斷。
為運行這個例子,這兩個平台都需要下載並安裝一些額外的bundle:
Felix
Config Admin (org.apache.felix.configadmin-1.2.4.jar)
SCR Declarative Services (org.apache.felix.scr-1.2.0.jar)
Equinox:
org.eclipse.equinox.ds
org.eclipse.equinox.util
org.eclipse.osgi.services
截止目前,你應該已經熟悉安裝和啟動bundles的過程了;如果沒有,請參考 靜態模塊化那篇文章。我們需要安裝上述bundle,以及我們的shortening服務。 下面是在Equinox環境下的操作過程,其中bundle放在/tmp目錄下:
$ java -jar org.eclipse.osgi_* -console
osgi> install file:///tmp/org.eclipse.osgi.services_3.2.0.v20090520-1800.jar
Bundle id is 1
osgi> install file:///tmp/org.eclipse.equinox.util_1.0.100.v20090520-1800.jar
Bundle id is 2
osgi> install file:///tmp/org.eclipse.equinox.ds_1.1.1.R35x_v20090806.jar
Bundle id is 3
osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar
Bundle id is 4
osgi> install file:///tmp/com.infoq.shorten.command- 1.1.0.jar
Bundle id is 5
osgi> install file:///tmp/com.infoq.shorten.tinyurl- 1.1.0.jar
Bundle id is 6
osgi> install file:///tmp/com.infoq.shorten.trim-1.1.0.jar
Bundle id is 7
osgi> start 1 2 3 4 5
osgi> shorten http://www.infoq.com
...
osgi> start 6 7
osgi> shorten http://www.infoq.com
http://tinyurl.com/yr2jrn
osgi> stop 6
osgi> shorten http://www.infoq.com
http://tr.im/HCRx
osgi> stop 7
osgi> shorten http://www.infoq.com
...
當我們安裝並啟動我們的依賴後(包括shorten命令),shorten命令仍不能 在控制台顯示結果。只有當我們啟動針對shorten命令所注冊的shortening服務 時才行。
當地一個shortening服務停止時,實現自動轉移至第二個shortening服務。 第二個服務也停掉的話,shorten command服務則自動清除注冊。
注意
聲明式服務讓連接OSGi服務更加容易。可是還有幾點需要注意。
DS bundle需要安裝並啟動,以把組件連接起來。這樣,DS bundle作為OSGi 框架啟動部分的一部分來安裝,比如Equinox的osgi.bundles或Felix的 felix.auto.start。
DS通常有其他依賴需要安裝。以Equinox為例,要包括equinox.util bundle 。
聲明式服務是OSGi Compendium Specification的一部分,而不是核心規范的 一部分,因此對於服務接口通常需要由一個獨立的bundle提供。在Equinox環境 下,是由osgi.services提供,但在Felix環境下,接口由SCR(Service Component Registry——服務組件注冊)bundle自身輸出。
聲明式服務可以用properties來配置。通常利用OSGi Config Admin服務;盡 管這是可選的。因此DS的有些部分需要運行Config Admin;實際上,Equinox 3.5有一個bug,如果要用Config Admin,它需要在DS(Declarative Services)之 前啟動。這往往要求使用start-up 屬性,以確保滿足正確的依賴。
OSGI-INF目錄(與XML文件一起)需要被包含進bundle中,否則DS看不到它。 你還需要確保Service-Component頭在bundle的manifest中存在。
還可能要用Service-Component: OSGI-INF/*.xml來包含所有組件而不是逐個 羅列其名字。這也允許fragment給一個bundle增加新組件。
bind和unbind方法需要synchronized以避免潛在的競爭情況出現,盡管在 AtomicReference之上使用compareAndSet()還可以被用作單個服務的non- synchronized占位符。
DS組件不需要OSGi接口,這樣,它可以在其他控制反轉模式(如Spring)裡 被模擬來測試或使用。可是Spring DM 和OSGi Blueprint服務都可用來組織服務 ,這就留作將來的話題吧。
DS 1.0 沒有定義默認的XML命名空間;DS 1.1 增加了 http://www.osgi.org/xmlns/scr/v1.1.0命名空間。如果文件中沒有出現命名空 間,就認為其兼容DS 1.0。
總結
本文中,我們討論了如何將我們的實現與OSGi API解耦,並使用哪些組件的 聲明式描述。聲明式服務提供了組織組件和注冊服務的能力,幫助避免啟動順序 依賴。另外,動態本質意味著當我們的依賴服務起停時,組件/服務也隨之起停 。
最後,無論使用DS還是手動管理服務,都使用的是相同的OSGi服務層以便通 信。因此,一個bundle可以通過手動方法提供服務,另一個可以用聲明式服務來 消費它(反之亦然)。我們應能夠混合並匹配1.0.0和1.1.0實現,並且它們應能 透明地工作。