在前一篇文章《模組化Java:靜態模組化》中,我們討論了如何構建Java模 塊並將其作為一個單獨的JAR進行部署。文中的例子給出了一個client和一個 server bundle(兩者在同一個VM中),client通過Factory 方法找到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將透明地綁定到新服務上。