1. 程式人生 > >Tomcat原始碼分析 (五)----- Tomcat 類載入器

Tomcat原始碼分析 (五)----- Tomcat 類載入器

在研究tomcat 類載入之前,我們複習一下或者說鞏固一下java 預設的類載入器。樓主以前對類載入也是懵懵懂懂,藉此機會,也好好複習一下。

樓主翻開了神書《深入理解Java虛擬機器》第二版,p227, 關於類載入器的部分。請看:

什麼是類載入機制?

Java虛擬機器把描述類的資料從Class檔案載入進記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以唄虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。

虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這動作的程式碼模組成為“類載入器”。

類與類載入器的關係

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠遠不限於類載入階段。對於任意一個類,都需要由載入他的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。這句話可以表達的更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來自同一個Class檔案,被同一個虛擬機器載入,只要載入他們的類載入器不同,那這個兩個類就必定不相等。

什麼是雙親委任模型?

  1. 從Java虛擬機器的角度來說,只存在兩種不同類載入器:一種是啟動類載入器(Bootstrap ClassLoader),這個類載入器使用C++語言實現(只限HotSpot),是虛擬機器自身的一部分;另一種就是所有其他的類載入器,這些類載入器都由Java語言實現,獨立於虛擬機器外部,並且全都繼承自抽象類java.lang.ClassLoader

    .

  2. 從Java開發人員的角度來看,類載入還可以劃分的更細緻一些,絕大部分Java程式設計師都會使用以下3種系統提供的類載入器:

    • 啟動類載入器(Bootstrap ClassLoader):這個類載入器複雜將存放在 JAVA_HOME/lib 目錄中的,或者被-Xbootclasspath 引數所指定的路徑種的,並且是虛擬機器識別的(僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib目錄下也不會過載)。
    • 擴充套件類載入器(Extension ClassLoader):這個類載入器由sun.misc.Launcher$ExtClassLoader實現,它負責夾雜JAVA_HOME/lib/ext 目錄下的,或者被java.ext.dirs 系統變數所指定的路徑種的所有類庫。開發者可以直接使用擴充套件類載入器。
    • 應用程式類載入器(Application ClassLoader):這個類載入器由sun.misc.Launcher$AppClassLoader 實現。由於這個類載入器是ClassLoader 種的getSystemClassLoader方法的返回值,所以也成為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫。開發者可以直接使用這個類載入器,如果應用中沒有定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

這些類載入器之間的關係一般如下圖所示:

圖中各個類載入器之間的關係成為 類載入器的雙親委派模型(Parents Dlegation Mode)。雙親委派模型要求除了頂層的啟動類載入器之外,其餘的類載入器都應當由自己的父類載入器載入,這裡類載入器之間的父子關係一般不會以繼承的關係來實現,而是都使用組合關係來複用父載入器的程式碼。

類載入器的雙親委派模型在JDK1.2 期間被引入並被廣泛應用於之後的所有Java程式中,但他並不是個強制性的約束模型,而是Java設計者推薦給開發者的一種類載入器實現方式。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,他首先不會自己去嘗試載入這個類,而是把這個請求委派父類載入器去完成。每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個請求(他的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

 為什麼要這麼做呢?

 如果沒有使用雙親委派模型,由各個類載入器自行載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並放在程式的ClassPath中,那系統將會出現多個不同的Object類, Java型別體系中最基礎的行為就無法保證。應用程式也將會變得一片混亂。

雙親委任模型時如何實現的?

非常簡單:所有的程式碼都在java.lang.ClassLoader中的loadClass方法之中,程式碼如下:

先檢查是否已經被載入過,若沒有載入則呼叫父載入器的loadClass方法, 如父載入器為空則預設使用啟動類載入器作為父載入器。如果父類載入失敗,丟擲ClassNotFoundException 異常後,再呼叫自己的findClass方法進行載入。

如何破壞雙親委任模型?

雙親委任模型不是一個強制性的約束模型,而是一個建議型的類載入器實現方式。在Java的世界中大部分的類載入器都遵循者模型,但也有例外,到目前為止,雙親委派模型有過3次大規模的“被破壞”的情況。
第一次:在雙親委派模型出現之前-----即JDK1.2釋出之前。
第二次:是這個模型自身的缺陷導致的。我們說,雙親委派模型很好的解決了各個類載入器的基礎類的統一問題(越基礎的類由越上層的載入器進行載入),基礎類之所以稱為“基礎”,是因為它們總是作為被使用者程式碼呼叫的API, 但沒有絕對,如果基礎類呼叫會使用者的程式碼怎麼辦呢?

這不是沒有可能的。一個典型的例子就是JNDI服務,JNDI現在已經是Java的標準服務,它的程式碼由啟動類載入器去載入(在JDK1.3時就放進去的rt.jar),但它需要呼叫由獨立廠商實現並部署在應用程式的ClassPath下的JNDI介面提供者(SPI, Service Provider Interface)的程式碼,但啟動類載入器不可能“認識“這些程式碼啊。因為這些類不在rt.jar中,但是啟動類載入器又需要載入。怎麼辦呢?

為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:執行緒上下文類載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader方法進行設定。如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過多的話,那這個類載入器預設即使應用程式類載入器。

嘿嘿,有了執行緒上下文載入器,JNDI服務使用這個執行緒上下文載入器去載入所需要的SPI程式碼,也就是父類載入器請求子類載入器去完成類載入的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類載入器,實際上已經違背了雙親委派模型的一般性原則。但這無可奈何,Java中所有涉及SPI的載入動作基本勝都採用這種方式。例如JNDI,JDBC,JCE,JAXB,JBI等。

第三次:為了實現熱插拔,熱部署,模組化,意思是新增一個功能或減去一個功能不用重啟,只需要把這模組連同類載入器一起換掉就實現了程式碼的熱替換。

Tomcat 的類載入器是怎麼設計的?

首先,我們來問個問題:

Tomcat 如果使用預設的類載入機制行不行?

我們思考一下: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 如何實現自己獨特的類載入機制?

我們看看他們的設計圖:

 

我們在這張圖中看到很多類載入器,除了Jdk自帶的類載入器,我們尤其關心Tomcat自身持有的類載入器。仔細一點我們很容易發現:Catalina類載入器和Shared類載入器,他們並不是父子關係,而是兄弟關係。為啥這樣設計,我們得分析一下每個類載入器的用途,才能知曉。

  1. Common類載入器,負責載入Tomcat和Web應用都複用的類
  2. Catalina類載入器,負責載入Tomcat專用的類,而這些被載入的類在Web應用中將不可見
  3. Shared類載入器,負責載入Tomcat下所有的Web應用程式都複用的類,而這些被載入的類在Tomcat中將不可見
  4. WebApp類載入器,負責載入具體的某個Web應用程式所使用到的類,而這些被載入的類在Tomcat和其他的Web應用程式都將不可見
  5. Jsp類載入器,每個jsp頁面一個類載入器,不同的jsp頁面有不同的類載入器,方便實現jsp頁面的熱插拔

原始碼閱讀

Tomcat啟動的入口在Bootstrap的main()方法main()方法執行前,必然先執行其static{}塊。所以我們首先分析static{}塊,然後分析main()方法

Bootstrap.static{}

static {
    // 獲取使用者目錄
    // Will always be non-null
    String userDir = System.getProperty("user.dir");

    // 第一步,從環境變數中獲取catalina.home,在沒有獲取到的時候將執行後面的獲取操作
    // Home first
    String home = System.getProperty(Globals.CATALINA_HOME_PROP);
    File homeFile = null;

    if (home != null) {
        File f = new File(home);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    // 第二步,在第一步沒獲取的時候,從bootstrap.jar所在目錄的上一級目錄獲取
    if (homeFile == null) {
        // First fall-back. See if current directory is a bin directory
        // in a normal Tomcat install
        File bootstrapJar = new File(userDir, "bootstrap.jar");

        if (bootstrapJar.exists()) {
            File f = new File(userDir, "..");
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }
    }

    // 第三步,第二步中的bootstrap.jar可能不存在,這時我們直接把user.dir作為我們的home目錄
    if (homeFile == null) {
        // Second fall-back. Use current directory
        File f = new File(userDir);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    // 重新設定catalinaHome屬性
    catalinaHomeFile = homeFile;
    System.setProperty(
            Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

    // 接下來獲取CATALINA_BASE(從系統變數中獲取),若不存在,則將CATALINA_BASE保持和CATALINA_HOME相同
    // Then base
    String base = System.getProperty(Globals.CATALINA_BASE_PROP);
    if (base == null) {
        catalinaBaseFile = catalinaHomeFile;
    } else {
        File baseFile = new File(base);
        try {
            baseFile = baseFile.getCanonicalFile();
        } catch (IOException ioe) {
            baseFile = baseFile.getAbsoluteFile();
        }
        catalinaBaseFile = baseFile;
    }
   // 重新設定catalinaBase屬性
    System.setProperty(
            Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}

我們把程式碼中的註釋搬下來總結一下:

  1. 獲取使用者目錄
  2. 第一步,從環境變數中獲取catalina.home,在沒有獲取到的時候將執行後面的獲取操作
  3. 第二步,在第一步沒獲取的時候,從bootstrap.jar所在目錄的上一級目錄獲取
  4. 第三步,第二步中的bootstrap.jar可能不存在,這時我們直接把user.dir作為我們的home目錄
  5. 重新設定catalinaHome屬性
  6. 接下來獲取CATALINA_BASE(從系統變數中獲取),若不存在,則將CATALINA_BASE保持和CATALINA_HOME相同
  7. 重新設定catalinaBase屬性

簡單總結一下,就是載入並設定catalinaHome和catalinaBase相關的資訊,以備後續使用。

main()

main方法大體分成兩塊,一塊為init,另一塊為load+start。

public static void main(String args[]) {
    // 第一塊,main方法第一次執行的時候,daemon肯定為null,所以直接new了一個Bootstrap物件,然後執行其init()方法
    if (daemon == null) {
        // Don't set daemon until init() has completed
        Bootstrap bootstrap = new Bootstrap();
        try {
            bootstrap.init();
        } catch (Throwable t) {
            handleThrowable(t);
            t.printStackTrace();
            return;
        }
        // daemon守護物件設定為bootstrap
        daemon = bootstrap;
    } else {
        // When running as a service the call to stop will be on a new
        // thread so make sure the correct class loader is used to prevent
        // a range of class not found exceptions.
        Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
    }

    // 第二塊,執行守護物件的load方法和start方法
    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null == daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }
}

我們點到init()裡面去看看~

public void init() throws Exception {
    // 非常關鍵的地方,初始化類載入器s,後面我們會詳細具體地分析這個方法
    initClassLoaders();

    // 設定上下文類載入器為catalinaLoader,這個類載入器負責載入Tomcat專用的類
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    // 暫時略過,後面會講
    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 使用catalinaLoader載入我們的Catalina類
    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 設定Catalina類的parentClassLoader屬性為sharedLoader
    // Set the shared extensions class loader
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    // catalina守護物件為剛才使用catalinaLoader載入類、並初始化出來的Catalina物件
    catalinaDaemon = startupInstance;
}

關鍵的方法initClassLoaders,這個方法負責初始化Tomcat的類載入器。通過這個方法,我們很容易驗證我們上一小節提到的Tomcat類載入圖。

private void initClassLoaders() {
    try {
        // 建立commonLoader,如果未建立成果的話,則使用應用程式類載入器作為commonLoader
        commonLoader = createClassLoader("common", null);
        if( commonLoader == null ) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        // 建立catalinaLoader,父類載入器為commonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        // 建立sharedLoader,父類載入器為commonLoader
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        // 如果建立的過程中出現異常了,日誌記錄完成之後直接系統退出
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}
所有的類載入器的建立都使用到了方法createClassLoader,所以,我們進一步分析一下這個方法。createClassLoader用到了CatalinaProperties.getProperty("xxx")方法,這個方法用於從conf/catalina.properties檔案獲取屬性值。
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
    // 獲取類載入器待載入的位置,如果為空,則不需要載入特定的位置,使用父類載入返回回去。
    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;
    // 替換屬性變數,比如:${catalina.base}、${catalina.home}
    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

   // 解析屬性路徑變數為倉庫路徑陣列
    String[] repositoryPaths = getPaths(value);

    // 對每個倉庫路徑進行repositories設定。我們可以把repositories看成一個個待載入的位置物件,可以是一個classes目錄,一個jar檔案目錄等等
    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(
                    new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(
                    new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(
                    new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(
                    new Repository(repository, RepositoryType.DIR));
        }
    }
    // 使用類載入器工廠建立一個類載入器
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

我們來分析一下ClassLoaderFactory.createClassLoader--類載入器工廠建立類載入器。

public static ClassLoader createClassLoader(List<Repository> repositories,
                                            final ClassLoader parent)
    throws Exception {

    if (log.isDebugEnabled())
        log.debug("Creating new class loader");

    // Construct the "class path" for this class loader
    Set<URL> set = new LinkedHashSet<>();
    // 遍歷repositories,對每個repository進行型別判斷,並生成URL,每個URL我們都要校驗其有效性,有效的URL我們會放到URL集合中
    if (repositories != null) {
        for (Repository repository : repositories)  {
            if (repository.getType() == RepositoryType.URL) {
                URL url = buildClassLoaderUrl(repository.getLocation());
                if (log.isDebugEnabled())
                    log.debug("  Including URL " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.DIR) {
                File directory = new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.DIR)) {
                    continue;
                }
                URL url = buildClassLoaderUrl(directory);
                if (log.isDebugEnabled())
                    log.debug("  Including directory " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.JAR) {
                File file=new File(repository.getLocation());
                file = file.getCanonicalFile();
                if (!validateFile(file, RepositoryType.JAR)) {
                    continue;
                }
                URL url = buildClassLoaderUrl(file);
                if (log.isDebugEnabled())
                    log.debug("  Including jar file " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.GLOB) {
                File directory=new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.GLOB)) {
                    continue;
                }
                if (log.isDebugEnabled())
                    log.debug("  Including directory glob "
                        + directory.getAbsolutePath());
                String filenames[] = directory.list();
                if (filenames == null) {
                    continue;
                }
                for (int j = 0; j < filenames.length; j++) {
                    String filename = filenames[j].toLowerCase(Locale.ENGLISH);
                    if (!filename.endsWith(".jar"))
                        continue;
                    File file = new File(directory, filenames[j]);
                    file = file.getCanonicalFile();
                    if (!validateFile(file, RepositoryType.JAR)) {
                        continue;
                    }
                    if (log.isDebugEnabled())
                        log.debug("    Including glob jar file "
                            + file.getAbsolutePath());
                    URL url = buildClassLoaderUrl(file);
                    set.add(url);
                }
            }
        }
    }

    // Construct the class loader itself
    final URL[] array = set.toArray(new URL[set.size()]);
    if (log.isDebugEnabled())
        for (int i = 0; i < array.length; i++) {
            log.debug("  location " + i + " is " + array[i]);
        }

    // 從這兒看,最終所有的類載入器都是URLClassLoader的物件~~
    return AccessController.doPrivileged(
            new PrivilegedAction<URLClassLoader>() {
                @Override
                public URLClassLoader run() {
                    if (parent == null)
                        return new URLClassLoader(array);
                    else
                        return new URLClassLoader(array, parent);
                }
            });
}

我們已經對initClassLoaders分析完了,接下來分析SecurityClassLoad.securityClassLoad,我們看看裡面做了什麼事情

public static void securityClassLoad(ClassLoader loader) throws Exception {
    securityClassLoad(loader, true);
}

static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception {

    if (requireSecurityManager && System.getSecurityManager() == null) {
        return;
    }

    loadCorePackage(loader);
    loadCoyotePackage(loader);
    loadLoaderPackage(loader);
    loadRealmPackage(loader);
    loadServletsPackage(loader);
    loadSessionPackage(loader);
    loadUtilPackage(loader);
    loadValvesPackage(loader);
    loadJavaxPackage(loader);
    loadConnectorPackage(loader);
    loadTomcatPackage(loader);
}

 private static final void loadCorePackage(ClassLoader loader) throws Exception {
    final String basePackage = "org.apache.catalina.core.";
    loader.loadClass(basePackage + "AccessLogAdapter");
    loader.loadClass(basePackage + "ApplicationContextFacade$PrivilegedExecuteMethod");
    loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedForward");
    loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedInclude");
    loader.loadClass(basePackage + "ApplicationPushBuilder");
    loader.loadClass(basePackage + "AsyncContextImpl");
    loader.loadClass(basePackage + "AsyncContextImpl$AsyncRunnable");
    loader.loadClass(basePackage + "AsyncContextImpl$DebugException");
    loader.loadClass(basePackage + "AsyncListenerWrapper");
    loader.loadClass(basePackage + "ContainerBase$PrivilegedAddChild");
    loadAnonymousInnerClasses(loader, basePackage + "DefaultInstanceManager");
    loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntry");
    loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntryType");
    loader.loadClass(basePackage + "ApplicationHttpRequest$AttributeNamesEnumerator");
}

這兒其實就是使用catalinaLoader載入tomcat原始碼裡面的各個專用類。我們大致羅列一下待載入的類所在的package:

  1. org.apache.catalina.core.*
  2. org.apache.coyote.*
  3. org.apache.catalina.loader.*
  4. org.apache.catalina.realm.*
  5. org.apache.catalina.servlets.*
  6. org.apache.catalina.session.*
  7. org.apache.catalina.util.*
  8. org.apache.catalina.valves.*
  9. javax.servlet.http.Cookie
  10. org.apache.catalina.connector.*
  11. org.apache.tomcat.*

好了,至此我們已經分析完了init裡面涉及到的幾個關鍵方法

WebApp類載入器

到這兒,我們隱隱感覺到少分析了點什麼!沒錯,就是WebApp類載入器。整個啟動過程分析下來,我們仍然沒有看到這個類載入器。它又是在哪兒出現的呢?

我們知道WebApp類載入器是Web應用私有的,而每個Web應用其實算是一個Context,那麼我們通過Context的實現類應該可以發現。在Tomcat中,Context的預設實現為StandardContext,我們看看這個類的startInternal()方法,在這兒我們發現了我們感興趣的WebApp類載入器。

protected synchronized void startInternal() throws LifecycleException {
    if (getLoader() == null) {
        WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }
}

入口程式碼非常簡單,就是webappLoader不存在的時候建立一個,並呼叫setLoader方法。

總結

我們終於完整地分析完了Tomcat的整個啟動過程+類載入過程。也瞭解並學習了Tomcat不同的類載入機制是為什麼要這樣設計,帶來的附加作用又是怎樣的。