1. 程式人生 > >可怕的執行緒上下文類裝載器(TCCL)

可怕的執行緒上下文類裝載器(TCCL)

在明天的 OSGi 2012 社群活動上,我將以“如何使你的類庫在不依賴 OSGi 的情況下進行友好地 OSGi”為主題進行演講。在演講中我將會提及 Java 的執行緒上下文類載入器(TCCL),但是整個演講只有 25 分鐘,我沒有更多時間對此進行深入討論。所以我寫這篇部落格希望能夠幫助大家瞭解到一些相關背景資訊。
本文中的很多技術資訊和研究取自於 Peter Kriens 先生寫的一篇沒有公開的 OSGi 需求建議書。對此我已經獲得了他的許可。

歷史

執行緒上下文類裝載器(TCCL)在 Java 體系中有一段非常有趣的歷史。
Java 定義了一套類裝載器的層次結構,在一個典型的 Java 執行時在底部的“application”裝載器負責“classpath”(通過 -classpath/-cp
提供),而啟動裝載器負責的是 rt.jar 包裡的一系列 JRE 類(譯者注:啟動裝載器處於類裝載器層次結構的最頂層,是所有類裝載器的父裝載器)。在中間還有一個名為“extension”的類裝載器,它負責裝載 JRE ext 目錄中的類。類裝載器實行雙親委派機制,也就是說應用類裝載器可以看到啟動類裝載器所裝載的所有的類,而啟動類裝載器卻看不到應用類裝載器所裝載的類。
Sun 的這一機制最先是在遠端方法呼叫(RMI)中實現 Java 序列化的時候碰到了問題。在從流中反序列化資料為 Java 物件時需要應用的類的相關資訊,但是反序列化程式碼是由啟動裝載器所裝載;根據雙親委派模型它也就訪問不到那些類。這個問題的最終解決是通過給 JRE 添加了私有的原生代碼,這些程式碼允許了對呼叫棧的檢查,通過檢查來找到第一個非 null 類的裝載器。
但是,很多其他擴充套件包隨後很快也遇到了同樣的問題,而且他們不能夠(也不應該!)都通過在 Sun JVM 中新增私有原生代碼的辦法來解決。大約在同一時間,J2EE 變得愈發重要。J2EE 是一個應用模型,其中的 Java 程式碼需要在一個嚴格約束的環境中才能執行。應用都執行在一個“倉庫”中。每個應用執行在一個獨立的類裝載器中,所有執行緒是不能夠跨應用/倉庫執行的。應用當然可以使用由 VM 提供的擴充套件包以及由容器提供的那些包,但是應用之間還是無法使用來自對方的任何程式碼。此外,這一模型還遇到了由容器提供的包無法訪問應用程式碼的問題。
為此 Java 1.2 引入了 TCCL:作為一個 tread-local 變數的和某個執行緒關聯在一起的一個類裝載器。這意味著什麼?這意味著任何包都可以在任意時候通過當前呼叫執行緒訪問“當前”上下文裝載器。這一上下文裝載器被期望予能夠訪問負責本次呼叫的特定應用類裝載器,然後能夠對應用的類進行訪問。
Sun 自己也開始大量使用 TCCL,儘管對此還沒有合適的規範。JDK 1.5 有 79 處引用到了 Thread.getContextClassLoader()
。Java 1.4 之後,Java 在 JNDI、JAXP、CORBA、JMX、Xalan、Xerces、AWT、Beans、SQL、Logging、Prefs、RMI、Security、Swing 以及大部分的 XML 子系統的虛擬機器實現上已經修改為使用上下文類裝載器。此外大部分中介軟體庫,比如 Hibernate、Saxon、Jakarta Commons Logging 等等也已開始使用 TCCL。Java 對 TCCL 的使用幾乎沒有提供任何規範或指南。這也就導致了幾乎每個庫都按照不同的策略對它的進行使用的後果。
關於正確使用 TCCL 的兩個關鍵問題是:
  • 什麼時候對它進行設定,由誰來設定?
  • 它應該能夠訪問哪些類?
基於 J2EE 的視角來看這些問題都比較容易回答,因為程式設計模型是受到約束的。容器掌控了所有的入口點(比如,它控制著執行 Servlet 的 HTTP 執行緒以及 EJB 的 RMI Socket 監聽執行緒,禁止建立額外執行緒,等等),因此它能夠保證 TCCL 總是在進入應用程式程式碼的入口處進行設定。由於應用程式是完全獨立的,因此這些能夠訪問的類的集合也就僅僅是該應用所擁有的所有類了。
但是在一個類似於 OSGi 的執行時模組化的場景下這些問題就很難回答了。bundle 能夠自由地建立它們自己的執行緒和切入點,並且也沒有通用的辦法來為一個“應用”確定為其提供類的 bundle 集合;的確,這裡“應用”的概念本身就難以準確定義。我們可以限制程式設計模型並且要求一個應用應該作為一個沒有任何依賴的獨立的 bundle 進行部署,但這樣做的話也就丟掉了使用 OSGi 的大部分好處。因此 OSGi 並沒有試圖去規定 TCCL 應該在什麼時候使用。

TCCL 的替代品

在我們自己的 OSGi 規範的程式碼裡,或者在任何顯式 OSGi 支援的庫裡,我們無需擔憂 TCCL,因為 OSGi 服務提供了一個遠比任何基於類裝載器途徑來進行裝載的途徑更輕便的途徑。但是(應用中)遺留的第三方庫依舊是一個問題。很多這樣的庫試圖在執行時根據名字去裝載應用的類:比如,Hibernate 從 .hbm.xml 檔案中讀取類名然後為每個資料庫記錄建立這些類的例項。
當你將這樣一個庫放到任何模組化的場景下時 - 包括 OSGi、JBoss 模組或者 Jigsaw - 你會發現行不通,因為一個類僅通過類名不足以對其進行唯一標識。一個類的識別由其完整描述名以及定義它的類裝載器構成(在 OSGi 中相當於包含它的 bundle)。因此作為類名的補充,我們還需要知道負責裝載該類的類裝載器。暨於由不同應用伺服器建立的種類繁多的類裝載環境,很多庫試圖使用一些探索式的方法來解決這個問題。TCCL 通常就是這樣一個探索式解決辦法之一,此外還有檢查該庫自己的類裝載器,使用 JRE 擴充套件類裝載器等等辦法。
如果一個庫只考慮 TCCL 的話會遇到一些阻礙:在我們該庫之前就要顯式地在我們的程式碼中設定 TCCL。幸運的是,這種情況很少發生,比如大多數這種庫也將呼叫 Class.forName(),這意味著該庫將會使用自己的類裝載器來裝載類。雖然這還遠遠不夠理想,但是我們可以通過部署一個單獨的片段來解決問題,而不是在我們的程式碼中散亂地呼叫 setContextClassLoader()。更好的做法當然是一個庫完全避開類名並允許類物件的傳遞;或者至少提供一個 API 方法來設定類裝載器以從中裝載類名。
不幸的是很難事先對此進行預測 - 至少沒有仔細檢查(庫)程式碼 - 該庫採用了何種探索式解決辦法,如上文所述這還是由於缺乏完整的規範和指南所造成的結果。

總結

我希望大家都能出席我明天的演講,該演講主要是針對想要自己的程式碼如何在 OSGi 中工作的更好的 Java 庫的程式設計師們,但也不會僅侷限於 OSGi。正如你從本文中能夠推斷出的那樣,避免 TCCL 以及其他基於類裝載器的怪異做法是實現友好 OSGi 的很重要的一點。此外我的演講中還會涉及服務動態化以及一些配置問題。希望明天能看到你們!
2012 年 10 月 23 日
原文連結:The Dreaded Thread Context Class Loader