程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 模塊化Java:動態模塊化

模塊化Java:動態模塊化

編輯:關於JAVA

在前一篇文章《模塊化Java:靜態模塊化》中,我們討論了如何構建Java模 塊並將其作為一個單獨的JAR進行部署。文中的例子給出了一個client和一個 server bundle(兩者在同一個VM中),client通過工廠方法找到server。在該 例子中,工廠實例化了一個已知類,當然也可以使用反射來獲取一個服務實現; Spring就是大量運用這種技術把spring對象綁定在一起的。

在我們討論動態服務之前,有必要回顧一下類路徑,因為標准Java代碼和模 塊化Java代碼的區別之一就是依賴在運行時是如何綁定的。在此之後,我們還將 簡單討論一下類的垃圾回收;如果你對此已非常熟悉,則可以跳過這部分內容。

Bundle ClassPath

對於一個普通Java程序,只有一個classpath——啟動應用程序所使用的那個 。該路徑通常是在命令行中用-classpath選項指定的,或者通 過CLASSPATH 環 境變量來設定。Java類裝載器在運行時解析類的時候會掃描此路徑,無論這一過 程是靜態地(已編譯進代碼)還是動態地(使用反射及 class.forName())。然 而,在運行時也可以使用多個類加載器;像Jetty和Tomcat這樣的Web應用引擎都 是使用多個類加載器,以便支持應用熱部署。

在OSGi中,每個bundle都有其自己的類加載器。需要被其他bundle訪問的類 則被委派(delegated)給這些其他bundle的類裝載器。因此,盡管在傳統應用 中,來自logging類庫、client和server JAR中的類都是由同一個類加載器加載 的,但在OSGi模塊系統中,他們都是由自己的類加載器加載的。

結果是,一個VM中有可能有多個類加載器,其中可能存在名字相同的不同 Class的對象。也就是說,在同一個VM中,一個叫做 com.infoq.example.App的 類,其不同版本可以由com.infoq.example bundle的第1版和第2版同時輸出。 Client bundle版本1使用該類的第1版,而client版本2使用該類的第2版。這在 模塊化系統中相當普遍;在同一個VM中,有些代碼可能需要裝載一個類庫的老版 本,同時更新點的代碼(在另一個bundle中)卻需要該類庫的新版本。好在OSGi 為你管理起這種依賴傳遞,確保不再出現不兼容類引發的問題。

類的垃圾回收

每個類都有一個對其類裝載器的引用。因此如果想要從不同的bundle訪問這 些類,不但要有對該類實例的引用,而且還要有對該類的類裝載器的引用。當一 個bundle持有另一個bundle的類時,它也會將該bundle固定在內存中。在前篇文 章的例子中,client被固定到該server上。

在靜態世界裡,無論你是否把自己的類固定到其他類(或類庫)都無所謂; 因為不會有什麼變化。可是,在動態世界裡,在運行時將類庫或工具替換成新版 本就有可能了。這聽起來可能有點復雜,但是在可熱部署應用的Web應用引擎早 期就出現了(如Tomcat,最早發布於1999年)。每個Web應用程序都綁定到 Servlet API的某個版本上,當其停止時,裝載該Web應用的類加載器也就廢棄掉 了。當Web應用重新被部署時,又創建了一個新的類加載器,新版類就由其裝載 。只要servlet引擎沒有保持對老版應用的引用,這些類就像其他Java對象一樣 被垃圾回收器回收了。

並不是所有的類庫都能意識到Java代碼中可能存在類洩漏的問題,就像是內 存洩漏。一個典型的例子就是Log4J的addAppender()調用,一旦其執行了,將會 把你的類綁定在Log4J bundle的生命周期上。即使你的bundle停止了,Log4J仍 將維對appender的引用,並繼續發送日志事件(除非該bundle在停止時恰當地調 用了removeAppender()方法)。

查找和綁定

為了成為動態,我們需要有一個能查找服務的機制,而不是持久持有他們( 以免bundle停止)。這是通過使用簡單Java接口和POJO來實現的,也就是大家所 熟知的services(注意他們與WS-DeathStar或其他任何XML底層架構都沒有關系 ;他們就是普通Java對象——Plain Old Java Objects)。

典型工廠實現方式是使用從properties文件中獲取的某種形式的類名,然後 用Class.forName()來實例化相應的類,OSGi則不同,它 維護了一個‘服務注冊 器’,其實這是一個包含了類名和服務的映射列表。這樣,OSGi系統就可以使用 context.getService(getServiceReference("java.sql.Driver")),而不是 class.forName("com.example.JDBCDriver")來獲取一個JDBC驅動器。這就把 client代碼解放出來了,它不需知道任何特定客戶端實現;相反,它可以在運行 時綁定任何可用驅動程序。移植到不同的數據庫服務器也就非常簡單了,只需停 止一個模塊並啟動一個新模塊;client甚至不需要重新啟動,也不需要改變任何 配置。

這樣做是因為client只需知道其所需的服務的API(基本上都是接口,盡管 OSGi規范允許使用其他類)。在上述情況中,接口名是 java.sql.Driver;返回 的接口實例是具體的數據庫實現(不必了解是哪些類,編碼在那裡)。此外,如 果服務不可用(數據庫不存在,或數據庫臨時停掉了),那麼這個方法會返回 null以說明該服務不可用。

為了完全動態,返回結果不應被緩存。換句話說,每當需要服務的時候,需 要重新調用getService。框架會在底層執行緩存操作,因此不存在太大的性能 問題。但重要的是,它允許數據庫服務在線被替換成新的服務,如果沒有緩存代 碼,那麼下次調用時,client將透明地綁定到新服務上。

付諸實施

為了證明這一點,我們將創建一個用於縮寫URL的OSGi服務。其思路是服務接 收一個長URL,如http://www.infoq.com/articles/modular-java-what-is-it, 將其轉換為短點的URL,如http://tr.im/EyH1。該服務也可以被廣泛應用在 Twitter這樣的站點上,還可以用它來把長URL轉成短的這樣便簽背後也寫得下。 甚至像《新科學家》和《Macworld》這樣的雜志也是用這些短URL來印刷媒體鏈 接的。

為了實現該服務,我們需要:

一個縮寫服務的接口

一個注冊為縮寫實現的bundle

一個驗證用client

盡管並沒有禁止把這些東西都放在同一個bundle中,但是我們還是把他們分 別放在不同的bundle裡。(即便他們在一個bundle中,最好也讓bundle通過服務 來通訊,就好像他們處於不同的bundle一樣;這樣他們就可以方便地與其他服務 提供者進行集成。

把縮寫服務接口與其實現(或client)分開放在單獨bundle中是很重要的。 該接口代表了client和server之間的‘共享代碼’,這樣,該接口在每個bundle 中都會加載。正因如此,每個bundle實際上都被固定到了該接口特定版本上,所 有服務都有共同的生命周期,將接口放在單獨 bundle中(在整個OSGi VM生命周 期中都在運行),我們的client就可以自由變化。如果我們把該接口放在某個服 務實現的bundle中,那麼該服務發生變化後我們就不能重新連接到client上了。

shorten接口的manifest和實現如下:

Bundle-ManifestVersion: 2
Bundle-Name: Shorten
Bundle-SymbolicName: com.infoq.shorten
Bundle-Version: 1.0.0
Export-Package: com.infoq.shorten
---
package com.infoq.shorten;

public interface IShorten {
  public String shorten(String url) throws IOException;
}

上面的例子建立了一個擁有單一接口(com.infoq.shorten.IShorten)的 bundle(com.infoq.shorten),並將其輸出給client。參數是一個URL,返回一 個唯一的壓縮版URL。

和接口定義相比,實現就相對有趣一些了。盡管最近縮寫名稱的應用開始多 起來了,但是所有這些應用的祖師爺都是 TinyURL.com。(具有諷刺意味的是, http://tinyurl.com實際上可以被壓縮的更短http://ow.ly/AvnC)。如今比較 流行有:ow.ly、bit.ly、tr.im等等。這裡並不是對這些服務全面介紹,也不是 為其背書,我們的實現也可以使用其他同類服務。本文之所以使用TinyURL和 Tr.im,是由於他們都可以匿名基於GET提交,易於實現,除此之外沒有其他原因 。

每種實現實際上都非常小;都以URL為參數(要縮寫的東西)並返回新的壓縮 過的文本:

package com.infoq.shorten.tinyurl;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import com.infoq.shorten.IShorten;

public class TinyURL implements IShorten {
  private static final String lookup =
  "http://tinyurl.com/api-create.php?url=";
  public String shorten(String url) throws IOException {
  String line = new BufferedReader(
   new InputStreamReader(
   new URL(lookup + url).openStream())).readLine();
  if(line == null)
   throw new IllegalArgumentException(
   "Could not shorten " + url);
  return line;
  }
}

Tr.im的實現類似,只需用http://api.tr.im/v1/trim_simple?url=替代 lookup的值即可。這兩種實現的源代碼分別在com.infoq.shorten.tinyurl和 com.infoq.shorten.trim bundle裡。

那麼,完成縮寫服務的實現後,我們如何讓其他程序訪問它呢?為此,我們 需要把實現注冊為OSGi框架的服務。BundleContext類的registerService (class,instance,properties)方法可以讓我們定義一個服務以供後用,該方法 通常在bundle的start()調用期間被調用。如上篇文章所講,我們必須定義一個 BundleActivator。實現該類後,我們還要把Bundle-Activator放在MANIFEST.MF 裡以便找到該實現。代碼如下:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: TinyURl
Bundle-SymbolicName: com.infoq.shorten.tinyurl
Bundle-Version: 1.0.0
Import-Package: com.infoq.shorten,org.osgi.framework
Bundle-Activator: com.infoq.shorten.tinyurl.Activator
---
package com.infoq.shorten.tinyurl;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.infoq.shorten.IShorten;

public class Activator implements BundleActivator {
  public void start(BundleContext context) {
  context.registerService(IShorten.class.getName(),
   new TinyURL(),null);
  }
  public void stop(BundleContext context) {
  }
}

盡管registerService()方法接收一個字符串作為其第一個參數,而且 用"com.infoq.shorten.IShorten"也是可以的,但是最好還是用 class.class.getName()這種形式,這樣如果你重構了包或改變了類名,在編譯 時就可發現問題。如果用字符串,進行了錯誤的重構,那麼只有在運行時你才能 知道問題所在。

registerService()的第二個參數是實例本身。之所以將其與第一個參數分開 ,是因為你可以將同一個服務實例輸出給多個服務接口(如果需要帶有版本的 API,這就有用了,你可以進化接口了)。另外,一個bundle輸出同一類型的多 個服務也是有可能的。

最後一個參數是服務屬性(service properties)。允許你給服務加上額外 元數據注解,比如標注優先級以表明該服務相對於其他服務的重要性,或者調用 者關心的其他信息(比如功能描述和廠商)。

只要該bundle一啟動,縮寫服務就可用了。當bundle停止,框架將自動取消 服務注冊。如果我們想要自己取消注冊(比方說,對錯誤代碼和網絡接口不可用 所作出的響應)也很容易(用context.unregisterService())。

使用服務

一旦服務起來並運行之後,我們就可以用client訪問它了。如果運行的是 Equinox,你可以用services命令羅列所有已安裝的服務,以及它們是由誰注冊 的:

{com.infoq.shorten.IShorten}={service.id=27}
  Registered by bundle: com.infoq.shorten.trim-1.0.0 [1]
  No bundles using service.
{com.infoq.shorten.IShorten}={service.id=28}
  Registered by bundle: com.infoq.shorten.tinyurl-1.0.0 [2]
  No bundles using service.

在調用服務處理URL之前,client需要解析服務。我們需要獲得一個服務引用 ,它可以讓我們查看服務自身內部的屬性,然後利用其來獲得我們感興趣的服務 。可是,我們需要能夠重復處理相同及不同的URL,以便我們可以把它集成到 Equinox或Felix的shell裡。實現如下:

package com.infoq.shorten.command;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import com.infoq.shorten.IShorten;

public class ShortenCommand {
  protected BundleContext context;
  public ShortenCommand(BundleContext context) {
  this.context = context;
  }
  protected String shorten(String url) throws  IllegalArgumentException, IOException {
  ServiceReference ref =
   context.getServiceReference(IShorten.class.getName());
  if(ref == null)
   return null;
  IShorten shorten = (IShorten) context.getService(ref);
  if(shorten == null)
   return null;
  return shorten.shorten(url);
  }
}

當shorten方法被調用時,上面這段程序將查找服務引用並獲得服務對象。然 後我們可以把服務對象賦值給一個IShorten對象,並使用它與前面講到的已注冊 服務進行交互。注意這些都是在同一個VM中發生的;沒有遠程調用,沒有強制異 常,沒有參數被序列化;只是一個POJO與另一個POJO對話。實際上,這裡與最開 始class.forName()例子的唯一區別是:我們如何獲得shorten POJO。

為了在Equinox和Felix裡面使用這一服務,我們需要放一些樣板代碼進去。 必須提一下,當我們定義manifest時,我們可以在 Felix和 Equinox命令行上聲 明可選依賴,這樣,當我們兩者中任何一個安裝之後,我們就可以運行了。(一 個更好的解決方案是將其部署為單獨的bundles,這樣我們可以去掉選項;但是 如果bundle不存在,activator將會失敗,因此無法啟動)。Equinox和Felix特 定命令的源代碼在com.infoq.shorten.command bundle中。

如果我們安裝了命令client bundle,我們將得到一個新命令,shorten,通 過OSGi shell可以調用它。要運行該命令,需要先執行java -jar equinox.jar -console -noExit或java -jar bin/felix.jar,然後安裝bundle,之後你就可 以使用該命令了:

java -jar org.eclipse.osgi_* -console -noExit 
osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar
Bundle id is 1
osgi> install file:///tmp/com.infoq.shorten.command- 1.0.0.jar
Bundle id is 2
osgi> install file:///tmp/com.infoq.shorten.tinyurl- 1.0.0.jar
Bundle id is 3
osgi> install file:///tmp/com.infoq.shorten.trim-1.0.0.jar
Bundle id is 4
osgi> start 1 2 3 4
osgi> shorten http://www.infoq.com
http://tinyurl.com/yr2jrn
osgi> stop 3
osgi> shorten http://www.infoq.com
http://tr.im/Eza8

注意,在運行時TinyURL和Tr.im服務都是可用的,但是一次只能使用一種服 務。可以設置一個服務級別(service ranking),這是一個整數,取值范圍在 Integer.MIN_VALUE和Integer.MAX_VALUE之間,當服務最初注冊時給 Constants.SERVICE_RANKING賦予相應值。值越大表示級別越高,當需要服務時 ,會返回最高級別的服務。如果沒有服務級別(默認值為 0),或者多個服務的 服務級別相同,那麼就使用自動分配的Constants.SERVICE_PID,可能是任意順 序的一個服務。

另一個需注意的問題是:當我們停止一個服務時,client會自動失敗轉移到 列表中的下一個服務。每當該命令執行時,它會獲取(當前)服務來處理 URL壓 縮需求。如果在運行期間服務提供程序發生了變化,不會影響命令的使用,只要 有此需求時有服務在就成。(如果你停止了所有服務提供程序,服務查找將返回 null,這將會打印出相應的錯誤信息——好的代碼應該確保程序能夠預防返回服 務引用為null的情況發生。)

服務跟蹤

除過每次查詢服務外,還可以用ServiceTracker來代替做這一工作。這就跳 過了中間獲得ServiceReference的幾步,但是要求你在構造之後調用open,以便 開始跟蹤服務。

對於ServiceReference,可以調用getService()獲得服務實例。而 waitForService()則在服務不可用時阻塞一段時間(根據指定的timeout。如果 timeout為0,則永遠阻塞)。我們可以如下重新實現shorten命令:

package com.infoq.shorten.command;

import java.io.IOException;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.infoq.shorten.IShorten;

public class ShortenCommand {
  protected ServiceTracker tracker;
  public ShortenCommand(BundleContext context) {
  this.tracker = new ServiceTracker(context,
   IShorten.class.getName(),null);
  this.tracker.open();
  }
  protected String shorten(String url) throws  IllegalArgumentException,
   IOException {
  try {
   IShorten shorten = (IShorten)
   tracker.waitForService(1000);
   if (shorten == null)
   return null;
   return shorten.shorten(url);
  } catch (InterruptedException e) {
   return null;
  }
  }
}

使用Service Tracker的常見問題是在構造後忘記了調用open()。除此之外, 還必須在MANIFEST.MF內部引入org.osgi.util.tracker包。

使用ServiceTracker來管理服務依賴通常被認為是管理關系的好方法。在沒 有使用服務的情況下,查找已輸出的服務稍微有點復雜:比如, ServiceReference在其被解析為一個服務之前突然變得不可用了。存在一個 ServiceReference的原因是,相同實例能夠在多個bundle間共享,而且它可以被 用來基於某些標准(手工)過濾服務。而且,它還可以使用過濾器來限制可用服 務的集合。

服務屬性和過濾器

當一個服務注冊時,可以將服務屬性一起注冊。大多情況下屬性可以為null ,但是也可以提供OSGi特定或關於URL的通用屬性。例如,我們想給服務分級以 便區分優先級。我們可以注冊Constants.SERVICE_RANKING(代表優先級的數值 ),作為最初注冊過程的一部分。我們可能還想放一些 client想知道的元數據 ,比如服務的主頁在哪兒,該站點的條款鏈接。為達此目的,我們需要修改 activator:

public class Activator implements BundleActivator {
  public void start(BundleContext context) {
  Hashtable properties = new Hashtable();
  properties.put(Constants.SERVICE_RANKING, 10);
  properties.put(Constants.SERVICE_VENDOR, "http://tr.im");
  properties.put("home.page", "http://tr.im");
  properties.put("FAQ", "http://tr.im/website/faqs");
  context.registerService(IShorten.class.getName(),
   new Trim(), properties);
  }
...
}

服務級別自動由ServiceTracker及其他對象來管理,但也可以用特定條件來 過濾。Filter是由LDAP風格的過濾器改編而來的,其使用了一種前綴表示法 (prefix notation)來執行多個過濾。雖然多數情況下你想提供類的名字 (Constants.OBJECTCLASS),但你也可以對值進行檢驗(包括限制連續變量的 取值范圍)。Filter是通過BundleContext創建的;如果你想跟蹤實現了 IShorten接口的服務,並且定義一個FAQ,我們可以這樣做:

...
public class ShortenCommand 
  public ShortenCommand(BundleContext context) {
  Filter filter = context.createFilter("(&" +
   "(objectClass=com.infoq.shorten.IShorten)" +
   "(FAQ=*))");
  this.tracker = new ServiceTracker(context,filter,null);
  this.tracker.open();
  }
  ...
}

在定義服務時可以被過濾或可以設置的標准屬性包括:

service.ranking (Constants.SERVICE_RANKING) - 整數,可以區分服務優 先級

service.id (Constants.SERVICE_ID) - 整數,在服務被注冊時由框架自動 設置

service.vendor (Constants.SERVICE_VENDOR) - 字符串,表明服務出自誰 手

service.pid (Constants.SERVICE_PID) - 字符串,代表服務的PID (persistent identifier)

service.description (Constants.SERVICE_DESCRIPTION) - 服務的描述

objectClass (Constants.OBJECTCLASS) - 接口列表,服務被注冊在哪些接 口下

過濾器語法在OSGi核心規范的 3.2.7節 “Filter syntax”中有定義。最基 本的,它允許如等於(=)、約等於(~=)、大於等於、小於等於以及子字符串 比較等操作符。括號將過流器分組,並且可以結合使用“&”、“|” 或“! ”分別代表and、or和not。屬性名不是大小寫敏感的,值可能是(如果不用~=作 比的話)。“*”是通配符,可用來支持子字符串匹配,比如 com.infoq.*.*。

總結

本文中,我們介紹了如何使用服務進行bundle間通信,以替代直接類引用的 方法。服務可以讓模塊系統動態化,這樣就能應對在運行時服務的變化問題。我 們還接觸到了服務級別、屬性及過濾器,並使用標准服務跟蹤器來更容易的訪問 服務並跟蹤變化的服務。我們將在下一部分介紹如何用聲明式服務使得服務的編 寫更加容易。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved