1. 程式人生 > >Java類載入器--Tomcat的類載入器架構

Java類載入器--Tomcat的類載入器架構

文章引用:

主流的Java Web伺服器(也就是Web容器),如Tomcat、Jetty、WebLogic、WebSphere或其他筆者沒有列舉的伺服器,都實現了自己定義的類載入器(一般都不止一個)。因為一個功能健全的Web容器,要解決如下幾個問題:

1)部署在同一個Web容器上的兩個Web應用程式所使用的Java類庫可以實現相互隔離。這是最基本的需求,兩個不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個伺服器中只有一份,伺服器應當保證兩個應用程式的類庫可以互相獨立使用。

2)部署在同一個Web容器上的兩個Web應用程式所使用的Java類庫可以互相共享。這個需求也很常見,例如,使用者可能有10個使用Spring組織的應用程式部署在同一臺伺服器上,如果把10份Spring分別存放在各個應用程式的隔離目錄中,將會是很大的資源浪費——這主要倒不是浪費磁碟空間的問題,而是指類庫在使用時都要被載入到Web容器的記憶體,如果類庫不能共享,虛擬機器的方法區就會很容易出現過度膨脹的風險。

3)Web容器需要儘可能地保證自身的安全不受部署的Web應用程式影響。目前,有許多主流的Java Web容器自身也是使用Java語言來實現的。因此,Web容器本身也有類庫依賴的問題,一般來說,基於安全考慮,容器所使用的類庫應該與應用程式的類庫互相獨立。

4)支援JSP應用的Web容器,大多數都需要支援HotSwap功能。我們知道,JSP檔案最終要編譯成Java Class才能由虛擬機器執行,但JSP檔案由於其純文字儲存的特性,執行時修改的概率遠遠大於第三方類庫或程式自身的Class檔案。而且ASP、PHP和JSP這些網頁應用也把修改後無須重啟作為一個很大的“優勢”來看待,因此“主流”的Web容器都會支援JSP生成類的熱替換,當然也有“非主流”的,如執行在生產模式(Production Mode)下的WebLogic伺服器預設就不會處理JSP檔案的變化。

由於存在上述問題,在部署Web應用時,單獨的一個Class Path就無法滿足需求了,所以各種Web容都“不約而同”地提供了好幾個Class Path路徑供使用者存放第三方類庫,這些路徑一般都以“lib”或“classes”命名。被放置到不同路徑中的類庫,具備不同的訪問範圍和服務物件,通常,每一個目錄都會有一個相應的自定義類載入器去載入放置在裡面的Java類庫。現在,就以Tomcat容器為例,看一看Tomcat具體是如何規劃使用者類庫結構和類載入器的。

在Tomcat目錄結構中,有3組目錄(“/common/”、“/server/”和“/shared/”)可以存放Java類庫,另外還可以加上Web應用程式自身的目錄“/WEB-INF/

”,一共4組,把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能載入的類都可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader自己能載入的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader載入到的類,但各個WebAppClassLoader例項之間相互隔離。而JasperLoader的載入範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丟棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的例項,並通過再建立一個新的Jsp類載入器來實現JSP檔案的HotSwap功能。

對於Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置檔案的server.loader和share.loader項後才會真正建立Catalina ClassLoader和Shared ClassLoader的例項,否則在用到這兩個類載入器的地方都會用Common ClassLoader的例項代替,而預設的配置檔案中沒有設定這兩個loader項,所以Tomcat 6.x順理成章地把/common、/server和/shared三個目錄預設合併到一起變成一個/lib目錄,這個目錄裡的類庫相當於以前/common目錄中類庫的作用。這是Tomcat設計團隊為了簡化大多數的部署場景所做的一項改進,如果預設設定不能滿足需要,使用者可以通過修改配置檔案指定server.loader和share.loader的方式重新啟用Tomcat 5.x的載入器架構。

Tomcat載入器的實現清晰易懂,並且採用了官方推薦的“正統”的使用類載入器的方式。如果讀者閱讀完上面的案例後,能完全理解Tomcat設計團隊這樣佈置載入器架構的用意,那說明已經大致掌握了類載入器“主流”的使用方式,那麼筆者不妨再提一個問題讓讀者思考一下:前面曾經提到過一個場景,如果有10個Web應用程式都是用Spring來進行組織和管理的話,可以把Spring放到Common或Shared目錄下讓這些程式共享。Spring要對使用者程式的類進行管理,自然要能訪問到使用者程式的類,而使用者的程式顯然是放在/WebApp/WEB-INF目錄中的,那麼被CommonClassLoader或SharedClassLoader載入的Spring如何訪問並不在其載入範圍內的使用者程式呢?如果研究過虛擬機器類載入器機制中的雙親委派模型,相信讀者可以很容易地回答這個問題。

分析:如果按主流的雙親委派機制,顯然無法做到讓父類載入器載入的類去訪問子類載入器載入的類,上面在類載入器一節中提到過通過執行緒上下文方式傳播類載入器。

答案是使用執行緒上下文類載入器來實現的,使用執行緒上下文載入器,可以讓父類載入器請求子類載入器去完成類載入的動作。看spring原始碼發現,spring載入類所用的Classloader是通過Thread.currentThread().getContextClassLoader()來獲取的,而當執行緒建立時會預設setContextClassLoader(AppClassLoader),即執行緒上下文類載入器被設定為AppClassLoader,spring中始終可以獲取到這個AppClassLoader(在Tomcat裡就是WebAppClassLoader)子類載入器來載入bean,以後任何一個執行緒都可以通過getContextClassLoader()獲取到WebAppClassLoader來getbean了。