Tomcat源碼分析-類加載器

              Tomcat自定義類加載器在其體系中起着舉足輕重的作用,瞭解類加載器這塊內容是很有意義的。
        比如目前我所在公司erp產品定製了自己的類加載器,實現了通過擴展的方式進行二次開發等。
        Tomcat針對不同場景也定製了自己的類加載器,下面是我對自定義類加載器在tomcat中是如何應用的一些思考。
        1、java是如何實現"雙親委派模型"的?這個模型的特點是什麼?理解這個模型的意義是什麼?
        2、Tomcat有哪些類加載器?分別在何時創建的?其用途?以及如何實現的?
        3、java的類加載器是如何與啓動Tomcat類加載器做連接的?需要注意哪些點?
        4、爲什麼要設置上下文類加載器?其核心起了什麼作用?Tomcat是如何使用的?
        5、如何查看運行期對象是被哪個類加載器加載的,以及對應的路徑?

首先來回答第一個問題
1、java是如何實現"雙親委派模型"的?這個模型的特點是什麼?理解這個模型的意義是什麼?
雙親委派模型如下
java設計了一個抽象類加載器ClassLoader,在這個抽象類中維護了一個指向parent的ClassLoader,是通過這種組合關係實現“雙親”的,代碼邏輯如下:
在HotSpot虛擬機中,提供了一個啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機的一部分。另外就是所有其他類加載器,這些類加載器是java語言實現,獨立於虛擬機外部,並且都繼承自抽象類java.lang.ClassLoader。
在Java中另外兩個常被提到類加載器是擴展類加載器(Extension ClassLoader)與應用程序類加載器(Application ClassLoader),這兩個類加載器是在初始化sun.misc.Launcher時進行實例化上面兩個類加載器並設置關係的,代碼如下:
從源碼註釋可知,這是用於啓動程序入口(即main方法)的類
      從這份源碼中,我們可以清楚知道擴展類加載器以及應用程序類加載器是如何創建的以及獲取類路徑的,從創建的角度證明擴展類加載器是應用程序類加載器的“父類”;結構上這兩個類加載器都是直接繼承了URLClassLoader,與我們自定義類加載器在繼承體系上並無差異,這兩個類加載器在加載類時,實則都是通過"父類"java.lang.ClassLoader的loadClass進行的,上面是對雙親委派模型實現的理解。

      這個模型的特點是什麼呢?
      (1)、單向的自下而上,這個特點決定了基點越低,查找範圍越廣;也是分支與分支之間具有隔離性的基礎。
(2)、主幹與分支特點。
(3)、不同分支之間具有隔離性,而好多場景需要這個特點支持,如不同應用可以用同一個jar的不同版本這種場景。
這種設計是有很多優勢的,既能保證核心代碼的安全,又可以方便擴展,靈活,支持的場景多,具體就不在此贅述了,深入理解Java虛擬機和網上一些文章都有描述。
模型是現實場景的抽象,理解了這個模型,首先對這個模型對應各種場景有了更深入的理解,其次對代碼的實現有更清晰的指導,而理解這塊代碼後會加深對這模型的理解,對實現自定義類加載器是有大大的好處的。

         在來回答第二個問題                                                                                                                                                  
       2、Tomcat有哪些類加載器?分別在何時創建的?如何實現的?以及其用途?
       首先奉上Tomcat類加載器結構圖,如下
                        
         
     Tomcat類加載器總的來講可以分爲三種。
     第一種可以理解爲"Tomcat系統類加載器",指加載實現Tomcat程序jar對應的類加載器,分別稱之爲CommonClassLoader,CatalinaClassLoader,SharedClassLoader,這三個對象對應的類實則同一個,只是根據common.loader,server.loader,shared.loader這三個類路徑信息實例化了三個類加載器,默認情況下server.loader,shared.loader的類路徑是未指定的,實則只是實例化了一個類加載器,這種情況下CatalinaClassLoader與SharedClassLoader都只是CommonClassLoader實例化對象的一個引用。這個過程是在Tomcat的引導類org.apache.catalina.startup.Bootstrap初始化時創建的,代碼現如下:
            
            
            
    
      在Tomcat7.0.57版本中是定義的org.apache.catalina.loader.StandardClassLoader實例化Tomcat系統類加載器,而有些其它的版本,則是直接使用java.net.URLClassLoader實例化Tomcat系統類加載器,具體實例化是在org.apache.catalina.startup.ClassLoaderFactory類中的createClassLoader方法中進行的。
      對於系統類加載器catalinaLoader加載的是實現Tomcat軟件本身相關的jar,在沒有定製tomcat情況下,該類加載器實則是委託其父加載器commonLoader加載全部jar的;實現tomcat軟件jar是放在tomcat安裝目錄/lib目錄下的,commonLoader類加載的類路徑正是此處,而tomcat使用catalinaLoader去加載而非直接commonLoader去加載原因也是很明顯的,在設計時把catalinaLoader定位成加載tomcat軟件jar相關的類加載器,因此如我們基於tomcat源碼進行一些擴展定製後,只要配置server.loader的類路徑信息,並把這些擴展定製的類打成jar包放到此目錄中即可。
      系統類加載器sharedLoader的父類加載器也是commonLoader,因此catalinaLoader與sharedLoader實則是在不同分支上,兩者具有隔離性;而sharedLoader的定位是加載不同應用公共的jar,因此sharedLoader是tomcat各應用類加載器(WebappClassLoader)的父加載器,當不同應用用到相同的jar,並且想只需加載一份時,只需要把這個jar放到shared.loader指定的類路徑即可,這樣不僅節約了磁盤的資源,更重要的是節省了JVM中的堆棧等資源,並且這樣設計實則把tomcat本身的資源與應用公共資源做了一層隔離,不用把應用公共的jar放到Tomcat安裝目錄\lib下,這樣設計職責更加分明。
      Bootstrap相當於tomcat啓動引導類,Tomcat真正進行初始化與啓動服務器的操作的類是org.apache.catalina.startup.Catalina,當然這樣設計的原因也是非常充分的,在後面在進行詳細贅述,用catalinaLoader顯示loadClass Catalina後,會把sharedLoader設置到Catalina的一個ClassLoader類型的成員變量中進行維護,並最終在創建應用類加載器時把其設置成parent父類加載器。
      第二種是Tomcat應用類加載器,是用來加載部署在tomcat中的應用的。Tomcat在設計容器對象時,就設計了一個應用加載器,而應用加載器則又會關聯一個應用類加載器,應用加載器是Tomcat裏面的一個組件,存在生命週期的管理,而應用類加載器則更像是這個組件裏面的一個核心部件,在應用(StandardContext)啓動的過程中(startInternal)會首先判斷有無應用加載器,沒有則創建一個,之後在啓動,在啓動的過程中創建應用類加載器,核心代碼如下:
                       
                        
        
     查看StandardContext啓動代碼可知,中間進行了幾次bindThread和unBindThread,bindThread主要是把WebappClassLoader設置成上下文類加載器,因爲應用加載器啓動創建完應用類加載器後,會觸發配置事件,進而解析應用下的web.xml,從而可能會實例化應用類加載器類路徑下(如web項目/WEB-INF/lib)中的類,web.xml的解析與注射工作是由Digester來完成的,而Digester在實例化對象時正是取的上下文的類加載器,調用棧如下:
           
       
     加載完應用後,通過unBindThread方法把老的類加載器catalinaLoader還原回上下文類加載器中,否則容易亂。
     Tomcat應用類加載器重寫了loadClass,而並不是直接調用父類的loadClass,但實現思路是很相似的,首先是判斷該類有無加載過,加載過則直接返回,沒有則讓j2se的類加載器先加載,以防web應用中定義了j2se重名的類覆蓋j2se中的類這種情況的發生,也是加載到了即返回,如沒有在根據是否有委託設置來決定讓當前類加載器來加載還是父類加載器來先加載,這個參數決定了當前類加載器與父類加載器加載的先後順序,只要其中一個加載到了就返回,如果都沒加載到就拋ClassNotFoundException,默認情況下是不委託的,當一個類既存在share.loader類路徑中又存在於應用類路徑中,那麼是否委託則顯得尤其重要了。
     第三種則是jsp對應的類加載器(JasperLoader),tomcat可實現對jsp動態加載,在tomcat啓動後,存在一個Mapper的數據結構存放訪問資源對應的Wrapper等信息。當訪問jsp資源時,連接器適配器CoyoteAdapter會根據請求信息以及一些附加信息封裝到org.apache.catalina.connector.Request和org.apache.catalina.connector.Response中,之後Mapper根據請求資源通過二分查找算法找到Mapper內部類各數據結構對應的唯一實例,在把這些實例中的信息一起封裝到新的數據結構MappingData中,並把MappingData對象設置到Request中,一個MappingData對象存放了一個url在tomcat處理所需經過的容器信息,設置完MappingData對象後,會進一步把MappingData部分信息封裝到Request相關變量中進行維護,如Context,Wrapper等信息,如資源是jsp,對應的Wrapper則爲StandardWrapper[jsp],請求最終通過連接器傳到容器管道中逐層處理。
     當index.jsp資源第一次被訪問時,會判斷\work\Catalina\localhost\_\org\apache\jsp\index_jsp.class是否存在jsp對應的class文件,如沒有,則會進行編譯操作,在編譯前會將jsp編譯器上下文(JspComplicationContext)關聯的加載器置空,進入JDTComplier生成class文件時(generateClass)會從jsp編譯上下文取類加載器,此時就會創建JSP類加載器,代碼如下:
             
             
    根據jsp文件生成java代碼調用棧如下:
             
      
    是否需要編譯或者重新編譯jsp文件,主要邏輯在編譯器的抽象類Compiler的isOutDated方法中,當jsp被訪問後,會根據命名規範把jsp生成的class在放在work目錄下;當重啓服務器後,對應的class文件還是存在的,此時再次訪問這個資源的時,如此時對應jsp文件未作修改的話,則會重新加載這個jsp對應的servlet,並進行初始化,而無需要重新編譯了,此時Jsp編譯器上下文對應的類加載器如未設置的話,則會重新創建一次,事實上這種場景也是需要在創建jsp類加載器的,調用棧如下:
            
    上面解答了JSP類加載器是何時,何種場景下創建的。
    下面來解答是如何創建的? 
    弄清楚這個jsp類加載器的創建過程實則弄清楚類路徑是如何指定的,以及父類加載是如何指定的,創建代碼如下:
             
      
    該方法是jsp編譯上下文(JspCompilationContext)中的一個方法,當jsp請求在對應StandardWrapperValue中做處理時,閥(StandardWrapperValue)通過所在的容器(StandardWrapper)獲取對應的servlet,如servlet未進行初始化則先進行初始化(init),初始化在整個生命週期中只進行一次,之後會根據請求,servlet信息創建一個過濾器鏈,並執行這個過濾鏈,如這個過濾器鏈中有設置過濾器,先執行過濾器,最後執行servlet。
    在JspServlet進行真正處理jsp時(指service方法),會根據JspServlt維護的JspRuntimeContext,ServletConfig,jspuri等對象包裝成一個JspServletWrapper新對象,在這個JspServletWrapper實例化過程中,就會實例化一個jsp編譯上下文(JspCompilationContext)對象,因此jsp編譯上下文是在tomcat啓動後處理請求的過程中實例化的,之後在判斷是否需要編譯,在這過程中會判斷輸出目錄是否爲空,爲空的話創建並且設置成類路徑,調用棧如下:
            
    上述是類路徑的指定過程。
    如jsp編譯上下文指定了類加載器(setClassLoader),則用該類加載器作爲jsp類加載器的父加載器,否則就取Jsp運行時(JspRuntimeContext)上下文關聯的parent類加載器,而JspRuntimeContext中維護的parent類加載器是在應用啓動的過程中初始化JspServlet中指定的,並且取的是上下文類加載器作爲parent的值,很明顯parent實則爲tomcat應用類加載器(WebappClassLoader),JspRuntimeContext維護的parent類加載器指定的調用棧如下:
             
       
    因而在處理jsp請求時就會通過這個閥(StandardWrapperValue)獲取對應容器(StandardWrapper[Jsp]),閥與容器的關係在實例化容器時就建立的,之後便獲取JspServlet進行處理jsp資源,如jsp資源需要編譯則就會在此過程中獲取JspRuntimeContext中維護好的parent的ClassLoader來實例Jsp類加載器。
    每個jsp資源對應着各自的JspServletWrapper,每個JspServletWrapper對應着各自的jsp編譯上下文(JspComplicationContext),而JspRuntimeContext是所有的JspServletWrapper的容器,代碼邏輯如下:
            
      
     上述是Jsp類加載器實例化過程中指定父類加載器涉及的相關過程。

     下面來回答第三個問題?
     3、java的類加載器是如何與啓動Tomcat類加載器做連接的?需要注意哪些點?
     當以調試源碼方式啓動tomcat時,Tomcat自身的類都是通過java的AppClassLoader加載的,因爲org.apache.catalina.startup.Bootstrap與其他包裏面的類都是在同一個類路徑下(tomcat源碼工程\bin 目錄);而通過批處理啓動安裝的tomcat時,org.apache.catalina.startup.Bootstrap是被AppClassLoader加載的,org.apache.catalina.startup.Catalina則是被catalinaLoader加載的;可以寫個jsp頁面作爲工具來獲取運行期指定類是被哪個類加載器所加載的,以及對應的物理路徑是哪。其實不難發現,Bootstrap是被打成jar放在tomcat安裝目錄/bin目錄下,而tomcat自身的其它類則是打成各種jar放在tomcat安裝目錄/lib目錄下,批處理在執行時,會把bin目錄也設置成了AppClassLoader的類路徑,因此這是要分別設計一個Bootstrap與Catalina的好處,要注意的是這兩個類在部署時是需要隔離的,並且需把Bootstrap所在的路徑設置成AppClassLoader的類路徑。

     下面則是第四個問題的解答。
     4、爲什麼要設置上下文類加載器?其核心起了什麼作用?Tomcat是如何使用的?
     在顯示loadClass時可以方便的從當前線程中拿到另外一個可用類加載器,如在java中提供了一些spi(Service Provider Interface)的接口,而實現是由其真正廠商實現的,因此spi的實現類可能沒有部署在AppClassLoader的類路徑下而可能是在其他容器的類路徑下或者在容器應用的類路徑下,因而可能需要指定的自定義類加載器去加載,而上下文類加載器是可設置更改的,因此十分適合這種場景的使用,上下文類加載器創建邏輯如下:
             
       在實例化一個線程類(Thread)時就會默認指定一個上下文類加載器,並且提供了setContextClassLoader方法進行更改。
     Tomcat通過Digester實例化對象時典型的應用了上下文類加載器,調用棧如下:
           
     在啓動上下文時等場景也有用到上下文類加載器。
     tomcat在設計類結構時,對程序入口(如Bootstrap,Catalina),容器對象,一些"運行時上下文"(指JspRuntimeContext)對象,Digester都提供了一個類加載器成員變量維護類加載器信息;當Tomcat新增線程處理請求時,新增線程此時上下文類加載器爲StandardClassLoader,當請求進入StandardHostValue處理時,則會取StandardContext中維護的類加載器爲該線程上下文類加載器,之後進行下一層的處理,處理完後在把老的類加載器重新設置回上下文類加載器中,在使用上下文類加載器時這些點都是需要注意的,部分代碼如下:
        
    
    最後來回答第五個問題。
    5、如何查看運行期對象是被哪個類加載器加載的?以及對應的路徑?
    寫個jsp頁面,核心代碼如下:
           
    放到tomcat其中一個普通應用中即可,之後通過如下類似url獲取運行期指定類的類加載器以及路徑。  
    http://localhost:8080/hello/getclassurl.jsp?className=org.apache.catalina.core.StandardContext
     
     此文Tomcat版本爲Tomcat7.0.57,不同版本有些地方可能有些差異。

           
   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章