歡迎您光臨本站 註冊首頁

Java動態模塊化運行原理與實踐(1)

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

  在本篇文章中,我們將詳細討論模塊化Java中的動態模塊化,包括對Bundle ClassPath、類的垃圾回收以及查找綁定等.

  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.example.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和實現如下:

  1.Bundle-ManifestVersion: 2

  2.Bundle-Name: Shorten

  3.Bundle-SymbolicName: com.infoq.shorten

  4.Bundle-Version: 1.0.0

  5.Export-Package: com.infoq.shorten

  6.---

  7.package com.infoq.shorten;

  8.

  9.public interface IShorten {

  10. public String shorten(String url) throws IOException;

  11.}

  上面的例子建立了一個擁有單一介面(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為參數(要縮寫的東西)並返回新的壓縮過的文本:

  12.package com.infoq.shorten.tinyurl;

  13.import java.io.BufferedReader;

  14.import java.io.InputStreamReader;

  15.import java.net.URL;

  16.import com.infoq.shorten.IShorten;

  17.

  18.public class TinyURL implements IShorten {

  19. private static final String lookup =

  20. "http://tinyurl.com/api-create.php?url=";

  21. public String shorten(String url) throws IOException {

  22. String line = new BufferedReader(

  23. new InputStreamReader(

  24. new URL(lookup url).openStream())).readLine();

  25. if(line == null)

  26. throw new IllegalArgumentException(

  27. "Could not shorten " url);

  28. return line;

  29. }

  30.}

  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里以便找到該實現.代碼如下:

  31.Manifest-Version: 1.0

  32.Bundle-ManifestVersion: 2

  33.Bundle-Name: TinyURL

  34.Bundle-SymbolicName: com.infoq.shorten.tinyurl

  35.Bundle-Version: 1.0.0

  36.Import-Package: com.infoq.shorten,org.osgi.framework

  37.Bundle-Activator: com.infoq.shorten.tinyurl.Activator

  38.---

  39.package com.infoq.shorten.tinyurl;

  40.import org.osgi.framework.BundleActivator;

  41.import org.osgi.framework.BundleContext;

  42.import com.infoq.shorten.IShorten;

  43.

  44.public class Activator implements BundleActivator {

  45. public void start(BundleContext context) {

  46. context.registerService(IShorten.class.getName(),

  47. new TinyURL(),null);

  48. }

  49. public void stop(BundleContext context) {

  50. }

  51.}

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

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

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

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


[火星人 ] Java動態模塊化運行原理與實踐(1)已經有430次圍觀

http://coctec.com/docs/java/show-post-60321.html