Tomcat打破雙親委派機制實現隔離Web應用的方法
Tomcat通過自定義類加載器WebAppClassLoader打破雙親委派,即重寫瞭JVM的類加載器ClassLoader的findClass方法和loadClass方法,以優先加載Web應用目錄下的類。
Tomcat負責加載我們的Servlet類、加載Servlet所依賴的JAR包。Tomcat本身也是個Java程序,因此它需要加載自己的類和依賴的JAR包。
若在Tomcat運行兩個Web應用程序,它們有功能不同的同名Servlet,Tomcat需同時加載和管理這兩個同名的Servlet類,保證它們不會沖突。所以Web應用之間的類需要隔離
若兩個Web應用都依賴同一三方jar,比如Spring,則Spring jar被加載到內存後,Tomcat要保證這兩個Web應用能共享之,即Spring jar隻被加載一次,否則隨著三方jar增多,JVM的內存會占用過大。
所以,和 JVM 一樣,需要隔離Tomcat本身的類和Web應用的類。
Tomcat類加載器的層次結構
Tomcat的類加載器層次結構
前三個是加載器實例名,不是類名。
WebAppClassLoader
若使用JVM默認的AppClassLoader加載Web應用,AppClassLoader隻能加載一個Servlet類,在加載第二個同名Servlet類時,AppClassLoader會返回第一個Servlet類的Class實例。
因為在AppClassLoader眼裡,同名Servlet類隻能被加載一次。
於是,Tomcat自定義瞭一個類加載器WebAppClassLoader, 並為每個Web應用創建一個WebAppClassLoader實例。
每個Web應用自己的Java類和依賴的JAR包,分別放在WEB-INF/classes
和WEB-INF/lib
目錄下,都是WebAppClassLoader加載的。
Context容器組件對應一個Web應用,因此,每個Context容器創建和維護一個WebAppClassLoader加載器實例。
不同加載器實例加載的類被認為是不同的類,即使類名相同。這就相當於在JVM內部創建相互隔離的Java類空間,每個Web應用都有自己的類空間,Web應用之間通過各自的類加載器互相隔離。
SharedClassLoader
兩個Web應用之間怎麼共享庫類,並且不能重復加載相同的類?
雙親委派機制的各子加載器都能通過父加載器去加載類,於是考慮把需共享的類放到父加載器的加載路徑。
應用程序即是通過該方式共享JRE核心類。
Tomcat搞瞭個類加載器SharedClassLoader,作為WebAppClassLoader的父加載器,以加載Web應用之間共享的類。
若WebAppClassLoader未加載到某類,就委托父加載器SharedClassLoader去加載該類,SharedClassLoader會在指定目錄下加載共享類,之後返回給WebAppClassLoader,即可解決共享問題。
CatalinaClassLoader
如何隔離Tomcat本身的類和Web應用的類?
兄弟關系:兩個類加載器是平行的,它們可能擁有同一父加載器,但兩個兄弟類加載器加載的類是隔離的。
於是,Tomcat搞瞭CatalinaClassLoader,專門加載Tomcat自身的類。
問題是,當Tomcat和各Web應用之間需要共享一些類時該怎麼辦?
CommonClassLoader
共享依舊靠父子關系。
再增加個CommonClassLoader,作為CatalinaClassLoader和SharedClassLoader的父加載器。
CommonClassLoader能加載的類都可被CatalinaClassLoader、SharedClassLoader 使用,而CatalinaClassLoader和SharedClassLoader能加載的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
Spring的加載問題
JVM默認情況下,若一個類由類加載器A加載,則該類的依賴類也由相同的類加載器加載。
比如Spring作為一個Bean工廠,它需要創建業務類的實例,並且在創建業務類實例之前需要加載這些類。Spring是通過調用Class.forName來加載業務類的,我們來看一下forName的源碼:
public static Class<?> forName(String className) { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
會使用調用者,即Spring的加載器去加載業務類。
Web應用之間共享的jar可交給SharedClassLoader加載,以避免重復加載。Spring作為共享的三方jar,本身由SharedClassLoader加載,Spring又要去加載業務類,按照前面那條規則,加載Spring的類加載器也會用來加載業務類,但是業務類在Web應用目錄下,不在SharedClassLoader的加載路徑下,這該怎麼辦呢?
線程上下文加載器
於是有瞭線程上下文加載器,一種類加載器傳遞機制。因為該類加載器保存在線程私有數據裡,隻要是同一個線程,一旦設置瞭線程上下文加載器,在線程後續執行過程中就能把這個類加載器取出來用。因此Tomcat為每個Web應用創建一個WebAppClassLoader類加載器,並在啟動Web應用的線程裡設置線程上下文加載器,這樣Spring在啟動時就將線程上下文加載器取出來,用來加載Bean。Spring取線程上下文加載的代碼如下:
cl = Thread.currentThread().getContextClassLoader();
在StandardContext的啟動方法,會將當前線程的上下文加載器設置為WebAppClassLoader。
啟動方法結束時,會恢復線程的上下文加載器:
Thread.currentThread().setContextClassLoader(originalClassLoader);
這是為什麼呢?
線程上下文加載器其實是線程的一個私有數據,跟線程綁定,這個線程完成啟動Context組件後,會被回收到線程池,之後被用來做其他事情,為瞭不影響其他事情,需恢復之前的線程上下文加載器。
優先加載web應用的類,當加載完瞭再改回原來的。
線程上下文的加載器就是指定子類加載器來加載具體的某個橋接類,比如JDBC的Driver的加載。
總結
Tomcat的Context組件為每個Web應用創建一個WebAppClassLoader類加載器,由於不同類加載器實例加載的類是互相隔離的,因此達到瞭隔離Web應用的目的,同時通過CommonClassLoader等父加載器來共享第三方JAR包。而共享的第三方JAR包怎麼加載特定Web應用的類呢?可以通過設置線程上下文加載器來解決。
多個應用共享的Java類文件和JAR包,分別放在Web容器指定的共享目錄:
CommonClassLoader
對應 <Tomcat>/common/*
CatalinaClassLoader
對應 <Tomcat >/server/*
SharedClassLoader
對應 <Tomcat >/shared/*
WebAppClassloader
對應 <Tomcat >/webapps/<app>/WEB-INF/*
可以在Tomcat conf目錄下的Catalina.properties文件裡配置各種類加載器的加載路徑。
當出現ClassNotFound錯誤時,應該檢查你的類加載器是否正確。
線程上下文加載器不僅僅可以用在Tomcat和Spring類加載的場景裡,核心框架類需要加載具體實現類時都可以用到它,比如我們熟悉的JDBC就是通過上下文類加載器來加載不同的數據庫驅動的。
到此這篇關於Tomcat打破雙親委派機制實現隔離Web應用的方法的文章就介紹到這瞭,更多相關Tomcat 隔離Web應用內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Tomcat Catalina為什麼不new出來原理解析
- 解析Tomcat架構原理到架構設計
- 淺談Tomcat如何打破雙親委托機制
- 詳解Java類加載器與雙親委派機制
- 傳統tomcat啟動服務與springboot啟動內置tomcat服務的區別(推薦)