1. 程式人生 > 其它 >Tomcat與spring的類載入器案例

Tomcat與spring的類載入器案例

Tomcat與spring的類載入器案例
接下來將介紹《深入理解java虛擬機器》一書中的案例,並解答它所提出的問題。(部分類容來自於書中原文)

Tomcat中的類載入器
在Tomcat目錄結構中,有三組目錄(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java類庫,此外還有第四組Web應用程式自身的目錄“/WEB-INF/*”,把java類庫放置在這些目錄中的含義分別是:

放置在common目錄中:類庫可被Tomcat和所有的Web應用程式共同使用。
放置在server目錄中:類庫可被Tomcat使用,但對所有的Web應用程式都不可見。
放置在shared目錄中:類庫可被所有的Web應用程式共同使用,但對Tomcat自己不可見。
放置在/WebApp/WEB-INF目錄中:類庫僅僅可以被此Web應用程式使用,對Tomcat和其他Web應用程式都不可見。
為了支援這套目錄結構,並對目錄裡面的類庫進行載入和隔離,Tomcat自定義了多個類載入器,這些類載入器按照經典的雙親委派模型來實現,如下圖所示


灰色背景的3個類載入器是JDK預設提供的類載入器,這3個載入器的作用前面已經介紹過了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 則是 Tomcat 自己定義的類載入器,它們分別載入 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 類庫。其中 WebApp 類載入器和 Jsp 類載入器通常會存在多個例項,每一個 Web 應用程式對應一個 WebApp 類載入器,每一個 JSP 檔案對應一個 Jsp 類載入器。

從圖中的委派關係中可以看出,CommonClassLoader 能載入的類都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能載入的類則與對方相互隔離。WebAppClassLoader 可以使用 SharedClassLoader 載入到的類,但各個 WebAppClassLoader 例項之間相互隔離。而 JasperLoader 的載入範圍僅僅是這個 JSP 檔案所編譯出來的那一個 Class,它出現的目的就是為了被丟棄:當伺服器檢測到 JSP 檔案被修改時,會替換掉目前的 JasperLoader 的例項,並通過再建立一個新的 Jsp 類載入器來實現 JSP 檔案的 HotSwap 功能。

Spring載入問題
Tomcat 載入器的實現清晰易懂,並且採用了官方推薦的“正統”的使用類載入器的方式。這時作者提一個問題:如果有 10 個 Web 應用程式都用到了spring的話,可以把Spring的jar包放到 common 或 shared 目錄下讓這些程式共享。Spring 的作用是管理每個web應用程式的bean,getBean時自然要能訪問到應用程式的類,而使用者的程式顯然是放在 /WebApp/WEB-INF 目錄中的(由 WebAppClassLoader 載入),那麼在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去載入並不在其載入範圍的使用者程式(/WebApp/WEB-INF/)中的Class呢?

解答
答案呼之欲出:spring根本不會去管自己被放在哪裡,它統統使用TCCL來載入類,而TCCL預設設定為了WebAppClassLoader,也就是說哪個WebApp應用呼叫了spring,spring就去取該應用自己的WebAppClassLoader來載入bean,簡直完美~

原始碼分析
有興趣的可以接著看看具體實現。在web.xml中定義的listener為org.springframework.web.context.ContextLoaderListener,它最終呼叫了org.springframework.web.context.ContextLoader類來裝載bean,具體方法如下(刪去了部分不相關內容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
try {
// 建立WebApplicationContext
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
// 將其儲存到該webapp的servletContext中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// 獲取執行緒上下文類載入器,預設為WebAppClassLoader
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
// 如果spring的jar包放在每個webapp自己的目錄中
// 此時執行緒上下文類載入器會與本類的類載入器(載入spring的)相同,都是WebAppClassLoader
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
// 如果不同,也就是上面說的那個問題的情況,那麼用一個map把剛才建立的WebApplicationContext及對應的WebAppClassLoader存下來
// 一個webapp對應一個記錄,後續呼叫時直接根據WebAppClassLoader來取出
currentContextPerThread.put(ccl, this.context);
}

return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
throw err;
}
}



具體說明都在註釋中,spring考慮到了自己可能被放到其他位置,所以直接用TCCL來解決所有可能面臨的情況。

總結
通過上面的兩個案例分析,我們可以總結出執行緒上下文類載入器的適用場景:

當高層提供了統一介面讓低層去實現,同時又要是在高層載入(或例項化)低層的類時,必須通過執行緒上下文類載入器來幫助高層的ClassLoader找到並載入該類。
當使用本類託管類載入,然而載入本類的ClassLoader未知時,為了隔離不同的呼叫者,可以取呼叫者各自的執行緒上下文類載入器代為託管。