1. 程式人生 > >Tomcat類載入器破壞雙親委派

Tomcat類載入器破壞雙親委派

轉載:https://blog.csdn.net/qq_38182963/article/details/78660779

http://www.cnblogs.com/aspirant/p/8991830.html

http://www.cnblogs.com/xing901022/p/4574961.html

雙親委派模式的破壞

雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前–即JDK1.2釋出之前。由於雙親委派模型是在JDK1.2之後才被引入的,而類載入器和抽象類java.lang.ClassLoader則是JDK1.0時候就已經存在,面對已經存在 的使用者自定義類載入器的實現程式碼,Java設計者引入雙親委派模型時不得不做出一些妥協。為了向前相容,JDK1.2之後的java.lang.ClassLoader添加了一個新的proceted方法findClass(),在此之前,使用者去繼承java.lang.ClassLoader的唯一目的就是重寫loadClass()方法,因為虛擬在進行類載入的時候會呼叫載入器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去呼叫自己的loadClass()。JDK1.2之後已不再提倡使用者再去覆蓋loadClass()方法,應當把自己的類載入邏輯寫到findClass()方法中,在loadClass()方法的邏輯裡,如果父類載入器載入失敗,則會呼叫自己的findClass()方法來完成載入,這樣就可以保證新寫出來的類載入器是符合雙親委派模型的。 

雙親委派模型的第二次“被破壞”是這個模型自身的缺陷所導致的,雙親委派模型很好地解決了各個類載入器的基礎類統一問題(越基礎的類由越上層的載入器進行載入),基礎類之所以被稱為“基礎”,是因為它們總是作為被呼叫程式碼呼叫的API。但是,如果基礎類又要呼叫使用者的程式碼,那該怎麼辦呢。 
這並非是不可能的事情,一個典型的例子便是JNDI服務,它的程式碼由啟動類載入器去載入(在JDK1.3時放進rt.jar),但JNDI的目的就是對資源進行集中管理和查詢,它需要呼叫獨立廠商實現部部署在應用程式的classpath下的JNDI介面提供者(SPI, Service Provider Interface)的程式碼,但啟動類載入器不可能“認識”之些程式碼,該怎麼辦? 
為了解決這個困境,Java設計團隊只好引入了一個不太優雅的設計:執行緒上下檔案類載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個;如果在應用程式的全域性範圍內都沒有設定過,那麼這個類載入器預設就是應用程式類載入器。了有執行緒上下文類載入器,JNDI服務使用這個執行緒上下文類載入器去載入所需要的SPI程式碼,也就是父類載入器請求子類載入器去完成類載入動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類載入器,已經違背了雙親委派模型,但這也是無可奈何的事情。Java中所有涉及SPI的載入動作基本上都採用這種方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

雙親委派模型的第三次“被破壞”是由於使用者對程式的動態性的追求導致的。為了實現熱插拔,熱部署,模組化,意思是新增一個功能或減去一個功能不用重啟,只需要把這模組連同類載入器一起換掉就實現了程式碼的熱替換。例如OSGi的出現。在OSGi環境下,類載入器不再是雙親委派模型中的樹狀結構,而是進一步發展為網狀結構。

Java 程式中基本有一個共識:OSGI對類載入器的使用時值得學習的,弄懂了OSGI的實現,就可以算是掌握了類載入器的精髓。

Tomcat類載入器

丟擲問題

1、既然 Tomcat 不遵循雙親委派機制,那麼如果我自己定義一個惡意的HashMap,會不會有風險呢?(阿里的面試官問

)

答: 顯然不會有風險,如果有,Tomcat都執行這麼多年了,那群Tomcat大神能不改進嗎? tomcat不遵循雙親委派機制,只是自定義的classLoader順序不同,但頂層還是相同的,

還是要去頂層請求classloader.

2、我們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題: 
1. 一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器只有一份,因此要保證每個應用程式的類庫都是獨立的,保證相互隔離。 
2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麼要有10份相同的類庫載入進虛擬機器,這是扯淡的。 
3. web容器也有自己依賴的類庫,不能於應用程式的類庫混淆。基於安全考慮,應該讓容器的類庫和程式的類庫隔離開來。 
4. web容器要支援jsp的修改,我們知道,jsp 檔案最終也是要編譯成class檔案才能在虛擬機器中執行,但程式執行後修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支援 jsp 修改後不用重啟。

再看看我們的問題:Tomcat 如果使用預設的類載入機制行不行? 
答案是不行的。為什麼?我們看,第一個問題,如果使用預設的類載入器機制,那麼是無法載入兩個相同類庫的不同版本的,預設的累加器是不管你是什麼版本的,只在乎你的全限定類名,並且只有一份。第二個問題,預設的類載入器是能夠實現的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎麼實現jsp檔案的熱修改(樓主起的名字),jsp 檔案其實也就是class檔案,那麼如果修改了,但類名還是一樣,類載入器會直接取方法區中已經存在的,修改後的jsp是不會重新載入的。那麼怎麼辦呢?我們可以直接解除安裝掉這jsp檔案的類載入器,所以你應該想到了,每個jsp檔案對應一個唯一的類載入器,當一個jsp檔案修改了,就直接解除安裝這個jsp類載入器。重新建立類載入器,重新載入jsp檔案。

Tomcat 如何實現自己獨特的類載入機制?

所以,Tomcat 是怎麼實現的呢?牛逼的Tomcat團隊已經設計好了。我們看看他們的設計圖:

我們看到,前面3個類載入和預設的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類載入器,它們分別載入/common/*/server/*/shared/*(在tomcat 6之後已經合併到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類載入器和Jsp類載入器通常會存在多個例項,每一個Web應用程式對應一個WebApp類載入器,每一個JSP檔案對應一個Jsp類載入器。

  • commonLoader:Tomcat最基本的類載入器,載入路徑中的class可以被Tomcat容器本身以及各個Webapp訪問;
  • catalinaLoader:Tomcat容器私有的類載入器,載入路徑中的class對於Webapp不可見;
  • sharedLoader:各個Webapp共享的類載入器,載入路徑中的class對於所有Webapp可見,但是對於Tomcat容器不可見;
  • WebappClassLoader:各個Webapp私有的類載入器,載入路徑中的class只對當前Webapp可見;

從圖中的委派關係中可以看出:

CommonClassLoader能載入的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能載入的類則與對方相互隔離。

WebAppClassLoader可以使用SharedClassLoader載入到的類,但各個WebAppClassLoader例項之間相互隔離。

而JasperLoader的載入範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丟棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的例項,並通過再建立一個新的Jsp類載入器來實現JSP檔案的HotSwap功能。

好了,至此,我們已經知道了tomcat為什麼要這麼設計,以及是如何設計的,那麼,tomcat 違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 我們前面說過:

雙親委派模型要求除了頂層的啟動類載入器之外,其餘的類載入器都應當由自己的父類載入器載入。

很顯然,tomcat 不是這樣實現,tomcat 為了實現隔離性,沒有遵守這個約定,每個webappClassLoader載入自己的目錄下的class檔案,不會傳遞給父類載入器。

我們擴展出一個問題:如果tomcat 的 Common ClassLoader 想載入 WebApp ClassLoader 中的類,該怎麼辦?

看了前面的關於破壞雙親委派模型的內容,我們心裡有數了,我們可以使用執行緒上下文類載入器實現,使用執行緒上下文載入器,可以讓父類載入器請求子類載入器去完成類載入的動作。

Tomcat類載入過程

tomcat的類載入機制是違反了雙親委託原則的,對於一些未載入的非基礎類(Object,String等),各個web應用自己的類載入器(WebAppClassLoader)會優先載入,載入不到時再交給commonClassLoader走雙親委託。具體的載入邏輯位於WebAppClassLoaderBase.loadClass()方法中,程式碼篇幅長,這裡以文字描述載入一個類過程:

  1. 先在本地快取中查詢是否已經載入過該類(對於一些已經載入了的類,會被快取在resourceEntries這個資料結構中),如果已經載入即返回,否則 繼續下一步。
  2. 讓系統類載入器(AppClassLoader)嘗試載入該類,主要是為了防止一些基礎類會被web中的類覆蓋,如果載入到即返回,返回繼續。
  3. 前兩步均沒載入到目標類,那麼web應用的類載入器將自行載入,如果載入到則返回,否則繼續下一步。
  4. 最後還是載入不到的話,則委託父類載入器(Common ClassLoader)去載入。

第3第4兩個步驟的順序已經違反了雙親委託機制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一樣是違反了雙親委託。