首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

模組化 Java:聲明式服務與 OSGi 元件模型

     編輯:Java 相關知識

前一篇文章《模組化 Java:動態模組化》描述如何透過服務 讓應用程式具備動態模組化特性。服務透過輸出一或多個可在運行時動態發現的介面實現。雖然這讓 client 和 server 完全解耦,但帶來一個問題:如何 (何時) 啟動服務?

啟動順序

在徹底動態系統中,服務不僅可在系統運行時裝卸,還可按不同順序啟動。有時這不是大問題:無論 A 和 B 啟動順序,若系統達到就緒狀態前無事件 (或執行緒) 出現,哪個服務先啟動無關緊要。

但許多情況不符合這簡單假設。經典例子是 logging:服務啟動和執行其他操作時,就要連接並開始寫日誌。若日誌服務此時不可用,後果如何?

假設服務能在運行時動態裝卸,client 應能應對服務不存在的情況。或許能切換到其他機制 (如輸出到標準輸出),或阻塞等待服務可用 (對 logging 系統非好答案)。但讓服務在啟動前就可用是不切實際的。

啟動層級

OSGi 提供機制控制 bundle 啟動順序,即 啟動層級。概念基於 UNIX 運行層級:系統以層級 1 啟動,單調遞增至目標啟動層級。每個 OSGi 容器提供不同預設目標層級:Equinox 預設 6,Felix 是 1。

啟動層級可建立 bundle 間啟動順序,讓關鍵 bundle 服務 (如 logging) 啟動層級比需要它的 bundle 更低。但因可用啟動層級值有限,且安裝程序傾向選擇單一數字作為啟動層級,無法確保僅靠啟動順序解決問題。

另一點:具有相同啟動層級的 bundle 各自獨立啟動 (可能並行),若某 bundle 與 log 服務同層級,無法保證 log 服務在需要時已就緒。啟動層級能解決大部分問題,但非全部。

聲明式服務

解決此問題的方案是 OSGi 聲明式服務 (以下稱 DS)。此方法下,各元件由外部 bundle 組織並決定何時可用。聲明式服務透過 XML 配置檔組織,描述需要 (消費) 或提供什麼服務。

上篇文章最後範例使用 ServiceTracker 獲取服務,必要時等待服務可用。若能延遲建立 shorten 命令直到 shortening 服務可用會很有用。

DS 定義 元件 概念,粒度比 bundle 細、比服務粗 (一個元件可消費/提供多個服務)。每個元件有名稱、對應一個 Java 類別,可透過呼叫該類別方法啟用或停用。與 OSGi Java API 不同,DS 允許用純 Java POJO 開發元件,完全無需程式碼層面依賴 OSGi。附帶好處是讓 DS 更易測試和模擬。

為說明此方法,繼續使用前面範例。需要兩個元件:shortening 服務本身、呼叫它的 ShortenCommand。

第一項任務:用 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="https://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 依賴;模擬實現驗證運作很輕鬆。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="https://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 服務可用,元件即實例化並連接服務。策略、基數性、服務下節說明。

策略與基數性

策略 可設 static 或 dynamic。static 表示一旦設置服務不變。若服務不可用,元件失效;若新服務出現,建立新實例並重新綁定。顯然比就地更新服務費勁。

用 dynamic 策略,IShorten 服務改變時,DS 對新服務呼叫 setShorten(),隨後對舊服務呼叫 unsetShorten()。DS 在 unset 前呼叫 set 以維持服務持續性。若替換服務先呼叫 unset,shorten 服務可能短暫為 null。這也是 unset 方法帶參數而非直接設為 null 的原因。

服務 基數性 預設 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

應熟悉安裝啟動 bundle 過程;否則參考靜態模組化文章。安裝上述 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 https://www.infoq.com
...
osgi> start 6 7
osgi> shorten https://www.infoq.com
https://tinyurl.com/yr2jrn
osgi> stop 6
osgi> shorten https://www.infoq.com
https://tr.im/HCRx
osgi> stop 7
osgi> shorten https://www.infoq.com
...

安裝啟動依賴 (含 shorten 命令) 後,shorten 命令仍無法在主控台顯示結果。只有啟動針對 shorten 命令註冊的 shortening 服務才行。

當第一個 shortening 服務停止,自動切換至第二個 shortening 服務。第二個也停掉,shorten command 服務自動清除註冊。

注意事項

  • DS bundle 需安裝啟動以連接元件。作為 OSGi 框架啟動一部分安裝,如 Equinox osgi.bundles 或 Felix felix.auto.start。
  • DS 通常有其他依賴需安裝。Equinox 例須含 equinox.util bundle。
  • 聲明式服務屬 OSGi Compendium Specification,非核心規範,服務介面通常需獨立 bundle 提供。Equinox 由 osgi.services 提供,Felix 由 SCR bundle 自身輸出。
  • 聲明式服務可用 properties 配置,常用 OSGi Config Admin 服務 (可選)。DS 部分需運行 Config Admin;Equinox 3.5 有 bug,若用 Config Admin 需在 DS 前啟動,常需 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 增加 https://www.osgi.org/xmlns/scr/v1.1.0。檔案無命名空間視為相容 DS 1.0。

總結

本文討論如何將實現與 OSGi API 解耦,使用元件聲明式描述。聲明式服務提供組織元件和註冊服務能力,幫助避免啟動順序依賴。動態本質意味著依賴服務起停時,元件/服務隨之起停。

無論用 DS 或手動管理服務,皆使用相同 OSGi 服務層通訊。因此一個 bundle 可手動提供服務,另一個用聲明式服務消費 (反之亦然)。應能混合匹配 1.0.0 和 1.1.0 實現,透明運作。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved