Tomcat源碼分析WebappClassLoader分析(基於8.0.5)

1、疑惑

在剛接觸 Tomcat 中的ClassLoader時心中不免冒出的疑惑: "Tomcat 裏面是怎麼樣設計ClassLoader的, 這樣設計有什麼好處?"; 我們先把這個問題留着, 到最後在看 !

2、java中ClassLoader類別

1. BootstrapClassLoader
    加載路徑: System.getProperty("java.class.path") 或直接通過 -Xbootclasspath 指定
    特性: 用C語言寫的
    手動獲取加載路徑: sun.misc.Launcher.getBootstrapClassPath().getURLs()

2. ExtClassLoader
    加載路徑: System.getProperty("java.ext.dirs") 或直接通過 -Djava.ext.dirs 指定
    特性: 繼承 URLClassLoader
    手動獲取加載路徑:((URLClassLoader)App.class.getClassLoader().getParent()).getURLs()


3. AppClassLoader
    加載路徑: System.getProperty("sun.boot.class.path") 或直接通過 -cp, -classpath 指定
    特性: 繼承 URLClassLoader
    手動獲取加載路徑: ((URLClassLoader)App.class.getClassLoader()).getURLs()
    通過 ClassLoader.getSystemClassLoader() 就可以獲取 AppClassLoader, 自己寫的程序中寫的 ClassLoader(繼承 URLClassLoader), 若不指定 parent, 默認的parent就是 AppClassLoader

PS:
AppClassLoader.getparent() = ExtClassLoader
ExtClassLoader.getParent() == null, 則直接通過 BootstrapClassLoader 來進行加載

3、java中ClassLoader主要方法

1. loadClass    方法 實現雙親委派模型
2. findClass    方法 根據Class名稱獲取Class路徑, 然後調用 defineClass 進行加載到JVM 內存中
3. defineClass  方法 加Class文件的二進制字節碼加載到JVM內存生成Class對象
4. resolveClass 方法 JVM規範裏面指連接操作中的第三步操作, 實際上我們的平時使用的JDK並沒有按照JVM的這個規範進行設計, 你在進行debug時, 發現這個 resolveClass 永遠是 false

4、ClassLoader.loadClass()方法

ClassLoader的雙親委派模式主要體現在 loadClass 方法上, 直接看代碼

synchronized (getClassLoadingLock(name)) {              // 1. 通過一個ClassName對應一個 Object, 放到 ConcurrentHashMap 中, 最終通過 synchronized 實現併發加載
    Class<?> c = findLoadedClass(name);                 // 2. 查看本 ClassLoader 是否加載過
    if (c == null) {
        try {
            if (parent != null) {                       // 4. parent != null, 則通過父ClassLoader來進行加載 (加載的原則是: class 一定要在 URLClassPath 中)
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);     // 5. parent == null, 則說明當前ClassLoader是ExtClassLoader, 直接通過 BootstrapClassLoader 來進行加載 (加載的原則是: class 一定要在 URLClassPath 中)
            }
        } catch (ClassNotFoundException e) {}
        if (c == null) {                                // 6. delegate 父 ClassLoader 還沒加載成功, 則用當前ClassLoader 來進行加載
            c = findClass(name);                        // 7. 通過 findClass 在本 ClassLoader 的path 上進行查找 class, 轉化成 byte[], 通過 defineClass 加載到內存中 (加載的原則是: class 一定要在 URLClassPath 中)
        }
    }
    if (resolve) {                                      // 8. 永遠的 resolve = false, JVM規範指定是通過 resolveClass 方法實現 鏈接 操作的第三步, 實際我們的JVM上並沒有實現這個操作
        resolveClass(c);
    }
    return c;
}

5、ClassLoader加載模式

下面通過一個簡單的Demo加深一下理解ClassLoader

Class A {
    public void doSomething(){
        B b = new B();
        b.doSomething();
    }

    public static void main(String[] args){
        A a = new A();
        a.doSomething()
    }
}

執行命令 java -classpath: test.jar A

操作步驟
1. AClass = AppClassLoader.loadClass(A)                                 # 通過 AppClassLoader 加載類A
2. BClass = AClass.getClassLoader().loadClass(B)                        # 其中通過 AClass.getClassLoader.getResource("/" + B.class.getName().replace(".", "/") + ".class") 查找 B 的Resource
3. BClass.getDeclaredMethod("doSomething").invoke(BClass.newInstance()) # 直接激活方法 doSomething

從中我們可以得知 在默認方法內進行 new 出對象, 其實是用的 Thread.currentThread().getContextClassloader() 來進行加載的 (A.class.getClassLoader() = B.class.getClassLoader()),有了上面的知識後我們再來看看 Tomcat 中的 ClassLoader。

6、Tomcat中ClassLoader的種類

1. BootstrapClassLoader : 系統類加載器
2. ExtClassLoader       : 擴展類加載器
3. AppClassLoader       : 普通類加載器
#下面是 這幾個 Classloader 是 Tomcat 對老版本的兼容
4. commonLoader         : Tomcat 通用類加載器, 加載的資源可被 Tomcat 和 所有的 Web 應用程序共同獲取
5. catalinaLoader       : Tomcat 類加載器, 加載的資源只能被 Tomcat 獲取(但 所有 WebappClassLoader 不能獲取到 catalinaLoader 加載的類)
6. sharedLoader         : Tomcat 各個Context的父加載器, 這個類是所有 WebappClassLoader 的父類, sharedLoader 所加載的類將被所有的 WebappClassLoader 共享獲取
7. WebappClassLoader    : 每個Context 對應一個 WebappClassloader, 主要用於加載 WEB-INF/lib 與 WEB-INF/classes 下面的資源

這個版本 (Tomcat 8.x.x) 中, 默認情況下 commonLoader = catalinaLoader = sharedLoader
(PS: 爲什麼這樣設計, 主要這樣這樣設計 ClassLoader 的層級後, WebAppClassLoader 就能直接訪問 tomcat 的公共資源, 若需要tomcat 有些資源不讓 WebappClassLoader 加載, 則直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 加載路徑就可以了)

在看看下面的 UML 圖, 加深一下理解:

從新再來看一下 ClassLoader 的初始化:

/**
 * 1. BootstrapClassLoader  : 系統類加載器
 * 2. ExtClassLoader        : 擴展類加載器
 * 3. AppClassLoader        : 普通類加載器
 #下面是 這幾個 Classloader 是 Tomcat 對老版本的兼容
 * 4. commonLoader      : Tomcat 通用類加載器, 加載的資源可被 Tomcat 和 所有的 Web 應用程序共同獲取
 * 5. catalinaLoader    : Tomcat 類加載器, 加載的資源只能被 Tomcat 獲取(但 所有 WebappClassLoader 不能獲取到 catalinaLoader 加載的類)
 * 6. sharedLoader      : Tomcat 各個Context的父加載器, 這個類是所有 WebappClassLoader 的父類, sharedLoader 所加載的類將被所有的 WebappClassLoader 共享獲取
 *
 * 這個版本 (Tomcat 8.x.x) 中, 默認情況下 commonLoader = catalinaLoader = sharedLoader
 * (PS: 爲什麼這樣設計, 主要這樣這樣設計 ClassLoader 的層級後, WebAppClassLoader 就能直接訪問 tomcat 的公共資源, 若需要tomcat 有些資源不讓 WebappClassLoader 加載, 則直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 加載路徑就可以了)
 */
private void initClassLoaders() {
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    try {                                                               // 1. 補充: createClassLoader 中代碼最後調用 new URLClassLoader(array) 來生成 commonLoader, 此時 commonLoader.parent = null,  則採用的是默認的策略 Launcher.AppClassLoader
        commonLoader = createClassLoader("common", null);               // 2. 根據 catalina.properties 指定的 加載jar包的目錄, 生成對應的 URLClassLoader( 加載 Tomcat 中公共jar包的 classLoader, 這裏的 parent 參數是 null, 最終 commonLoader.parent 是 URLClassLoader)
        if( commonLoader == null ) {                                    // 3. 若 commonLoader = null, 則說明在 catalina.properties 裏面 common.loader 是空的
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);     // 4. 將 commonClassLoader 作爲父 ClassLoader, 生成 catalinaLoader,這個類就是加載 Tomcat bootstrap.jar, tomcat-juli.jar 包的 classLoader (PS; 在 catalina.properties 裏面 server.loader 是空的, 則代碼中將直接將 commonLoader 賦值給 catalinaLoader)
        sharedLoader = createClassLoader("shared", commonLoader);       // 5. 將 commonClassLoader 作爲父 ClassLoader, 生成 sharedLoader, 這個類最後會作爲所有 WebappClassLoader 的父類 ( PS: 因爲 catalina.properties 裏面 shared.loader 是空的, 所以代碼中直接將 commonLoader 賦值給 sharedLoader)
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

額, 漏了, 漏了一個 JasperLoader, 這個classLoader 直接繼承 URLClassLoader, 當程序將 JSP 編譯成 servlet 的class之後, 通過這個 JasperLoader 進行加載(PS: 這個 JasperLoader 其實沒有什麼太多的功能); 接下來我們主要看 WebappClassLoader

7、WebappClassLoader常見屬性

protected final Matcher packageTriggersDeny = Pattern.compile(                          // 在 delegating = false 的情況下, 被這個正則匹配到的 class 不會被 WebappClassLoader 進行加載 (其實就是 Tomcat 中的代碼不能被 WebappClassLoader 來加載)
        "^javax\\.el\\.|" +
        "^javax\\.servlet\\.|" +
        "^org\\.apache\\.(catalina|coyote|el|jasper|juli|naming|tomcat)\\."
        ).matcher("");

protected final Matcher packageTriggersPermit =                                         // 在 delegating = false 的情況下, 下面正則匹配到的類會被 WebappClassLoader 進行加載
        Pattern.compile("^javax\\.servlet\\.jsp\\.jstl\\.").matcher("");

protected final ClassLoader parent;                                                     // WebappClassLoader 的父 parent(在這裏 Tomcat 8.x.x, parent  其實就是 commonClassloader)
protected final ClassLoader j2seClassLoader;                                            // 這個 classLoader 其實就是 ExtClassLoader (PS: 所有的 WebappClassLoader 出發到加載 J2SE 的類時, 直接通過 ExtClassLoader / BootstrapClassLoader 來進行加載 )
                                                                                
protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>(); // 加載資源的時候會將 文件緩存在這個 Map 裏面, 下次就可以根據 ResourceEntry.lastModified 來判斷是否需要熱部署

protected WebResourceRoot resources = null;                                             // 這個 WebappClassLoader 加載的資源(PS: 其實就是 StandardRoot, 在WebappClassLoader 啓動時, 會載入 WEB-INF/lib 與 WEB-INF/classes 下的資源de URL加入 WebAppClassLoader的 URLClassPath 裏面)

private final HashMap<String,Long> jarModificationTimes = new HashMap<>();              // 保存每個加載的資源, 上次修改的時間 (後臺定時任務檢查這個修改時間, 決定是否需要 reload)

8、WebappClassLoader構造函數

parent: WebappClassLoader 的父classLoader, j2seClassLoader: ExtClassLoader, 所有WebappClassLoader 加載 J2SE 的類時, 需通過 ExtClassLoader 或 BootstartpClassLoader 來進行加載。

public WebappClassLoader(ClassLoader parent) {              // 1. 在 Tomcat 8.x.x 中運行時, 會發現 parent 就是 commonClassLoader

    super(new URL[0], parent);

    ClassLoader p = getParent();                            // 2. 這裏做個檢查, 若構造函數傳來的 parent 是 null, 則 將 AppClassLoader 賦值給 WebAppClassLoader 的 parent
    if (p == null) {
        p = getSystemClassLoader();
    }
    this.parent = p;
                                                            // 3. 下面幾步是 獲取 Launcher.ExtClassLoader 賦值給 j2seClassLoader (主要是在類加載時會被用到)
    ClassLoader j = String.class.getClassLoader();
    if (j == null) {
        j = getSystemClassLoader();
        while (j.getParent() != null) {
            j = j.getParent();
        }
    }
    this.j2seClassLoader = j;                               // 4. 這裏進行賦值的就是 Launcher.ExtClassLoader

    securityManager = System.getSecurityManager();          // 5. 這裏的操作主要是判斷 Java 程序是否啓動安全策略
    if (securityManager != null) {
        refreshPolicy();
    }
}

9、WebappClassLoader start方法

/**
 * Start the class loader.
 *
 * @exception LifecycleException if a lifecycle error occurs
 * 將 /WEB-INF/classes 及 /WEB-INF/lib 封裝成 URL 加入到 ClassLoader 的 URLClassPath 裏面
 */
@Override
public void start() throws LifecycleException {
                                                                            // 下面的 resources 其實就是  StandardRoot
                                                                            // WebappClassLoader 進行資源/類 URL 的加載操作 (/WEB-INF/classes  與 WEB-INF/lib 下面資源的 URL)
    WebResource classes = resources.getResource("/WEB-INF/classes");        // 1. 加入 /WEB_INF/classes 的 URL
    if (classes.isDirectory() && classes.canRead()) {
        addURL(classes.getURL());
    }                                                                       // 2. 加入 /WEB_INF/lib 下面的 jar 的URL 加入 URLClassPath
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
            addURL(jar.getURL());                                           // 3. 這一步就是將 ClassLoader需要加載的 classPath 路徑 加入到 URLClassLoader.URLClassPath 裏面
            jarModificationTimes.put(                                       // 4. 放一下 jar 文件的 lastModified
                    jar.getName(), Long.valueOf(jar.getLastModified()));
        }
    }
}

這個方法其實就是將 /WEB-INF/classes 及 /WEB-INF/lib 封裝成 URL 加入到 ClassLoader 的 URLClassPath 裏面(PS: 當WebappClassloader在加載Class時, 通過這個URLs來決定是否加載 class )

10、WebappClassLoader modified方法

Tomcat 後來會啓用定時任務, 來檢查已經加載的資源是否有修改/增加/刪減, 來觸發 StandardContext 的 reload; 見代碼

/**
 * Have one or more classes or resources been modified so that a reload
 * is appropriate?
 */
// 校驗 WebappClassLoader 加載的資源是否有修改過, 若有文件修改過, 則進行熱部署
public boolean modified() {

    if (log.isDebugEnabled())
        log.debug("modified()");

    for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) {       // 1. 遍歷已經加載的資源
        long cachedLastModified = entry.getValue().lastModified;
        long lastModified = resources.getClassLoaderResource(
                entry.getKey()).getLastModified();                                  // 2. 對比 file 的 lastModified的屬性
        if (lastModified != cachedLastModified) {                                   // 3. 若修改時間不對, 則說明文件被修改過, StandardContext 需要重新部署
            if( log.isDebugEnabled() )
                log.debug(sm.getString("webappClassLoader.resourceModified",
                        entry.getKey(),
                        new Date(cachedLastModified),
                        new Date(lastModified)));
            return true;
        }
    }

    // Check if JARs have been added or removed
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    // Filter out non-JAR resources

    int jarCount = 0;
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {      // 4. 比較 /WEB-INF/lib 下的 jar 包是否有修改/增加/減少
            jarCount++;                                                              // 5. 記錄 /WEB-INF/lib 下的 jar 的個數
            Long recordedLastModified = jarModificationTimes.get(jar.getName());
            if (recordedLastModified == null) {
                // Jar has been added
                log.info(sm.getString("webappClassLoader.jarsAdded",
                        resources.getContext().getName()));
                return true;
            }
            if (recordedLastModified.longValue() != jar.getLastModified()) {        // 6. 比較一下這次的文件修改時間 與 上次文件的修改時間是否一樣, 不一樣的話, 直接返回 true, StandardContext 需要重新部署
                // Jar has been changed
                log.info(sm.getString("webappClassLoader.jarsModified",
                        resources.getContext().getName()));
                return true;
            }
        }
    }

    if (jarCount < jarModificationTimes.size()){                                 // 7. 判斷 WebappClassloader文件是夠有增加/減少, 若有變化的話, 直接返回 true, StandardContext 需要重新部署
        log.info(sm.getString("webappClassLoader.jarsRemoved",
                resources.getContext().getName()));
        return true;
    }


    // No classes have been modified
    return false;
}

11、WebappClassLoader loadClass方法

雙親委派模式的開關: WebappClassLoader 的loadClass有一個標識(delegateLoad) 用來控制是否啓用雙親委派模式;
下面來看方法的主要步驟:

 1. 判斷當前運用是否已經啓動, 未啓動, 則直接拋異常
 2. 調用 findLocaledClass0 從 resourceEntries 中判斷 class 是否已經加載 OK
 3. 調用 findLoadedClass(內部調用一個 native 方法) 直接查看對應的 WebappClassLoader 是否已經加載過
 4. 調用 binaryNameToPath 判斷是否 當前 class 是屬於 J2SE 範圍中的, 若是的則直接通過 ExtClassLoader, BootstrapClassLoader 進行加載 (這裏是雙親委派)
 5. 在設置 JVM 權限校驗的情況下, 調用 securityManager 來進行權限的校驗(當前類是否有權限加載這個類, 默認的權限配置文件是 ${catalina.base}/conf/catalina.policy)
 6. 判斷是否設置了雙親委派機制 或 當前 WebappClassLoader 是否能加載這個 class (通過 filter(name) 來決定), 將最終的值賦值給 delegateLoad
 7. 根據上一步中的 delegateLoad 來決定是否用 WebappClassloader.parent(也就是 sharedClassLoader) 來進行加載, 若加載成功, 則直接返回
 8. 上一步若未加載成功, 則調用 WebappClassloader.findClass(name) 來進行加載
 9. 若上一還是沒有加載成功, 則通過 parent 調用 Class.forName 來進行加載
 10. 若還沒加載成功的話, 那就直接拋異常

直接看代碼

 public synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {

    if (log.isDebugEnabled())
        log.debug("loadClass(" + name + ", " + resolve + ")");
    Class<?> clazz = null;

    // Log access to stopped classloader                                     // 1.  判斷程序是否已經啓動了, 未啓動 OK, 就進行加載, 則直接拋異常
    if (!started) {
        try {
            throw new IllegalStateException();
        } catch (IllegalStateException e) {
            log.info(sm.getString("webappClassLoader.stopped", name), e);
        }
    }

    // (0) Check our previously loaded local class cache
                                                                             // 2. 當前對象緩存中檢查是否已經加載該類, 有的話直接返回 Class
    clazz = findLoadedClass0(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // (0.1) Check our previously loaded class cache
                                                                             // 3. 是否已經加載過該類 (這裏的加載最終會調用一個 native 方法, 意思就是檢查這個 ClassLoader 是否已經加載過對應的 class 了哇)
    clazz = findLoadedClass(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // (0.2) Try loading the class with the system class loader, to prevent // 代碼到這裏發現, 上面兩步是 1. 查看 resourceEntries 裏面的信息, 判斷 class 是否加載過, 2. 通過 findLoadedClass 判斷 JVM 中是否已經加載過, 但現在 直接用 j2seClassLoader(Luancher.ExtClassLoader 這裏的加載過程是雙親委派模式) 來進行加載
    //       the webapp from overriding J2SE classes                        // 這是爲什麼呢 ? 主要是 這裏直接用 ExtClassLoader 來加載 J2SE 所對應的 class, 防止被 WebappClassLoader 加載了
    String resourceName = binaryNameToPath(name, false);                    // 4. 進行 class 名稱 轉路徑的操作 (文件的尾綴是 .class)
    if (j2seClassLoader.getResource(resourceName) != null) {                // 5. 這裏的 j2seClassLoader 其實就是 ExtClassLoader, 這裏就是 查找 BootstrapClassloader 與 ExtClassLoader 是否有權限加載這個 class (通過 URLClassPath 來確認)
        try {
            clazz = j2seClassLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    // (0.5) Permission to access this class when using a SecurityManager   // 6. 這裏的 securityManager 與 Java 安全策略是否有關, 默認 (securityManager == null), 所以一開始看代碼就不要關注這裏
    if (securityManager != null) {
        int i = name.lastIndexOf('.');
        if (i >= 0) {
            try {
                securityManager.checkPackageAccess(name.substring(0,i));   // 7. 通過 securityManager 對 是否能加載 name 的權限進行檢查 (對應的策略都在 ${catalina.base}/conf/catalina.policy 裏面進行定義)
            } catch (SecurityException se) {
                String error = "Security Violation, attempt to use " +
                    "Restricted Class: " + name;
                log.info(error, se);
                throw new ClassNotFoundException(error, se);
            }
        }
    }

    boolean delegateLoad = delegate || filter(name);                      // 8. 讀取 delegate 的配置信息, filter 主要判斷這個 class 是否能由這個 WebappClassLoader 進行加載 (false: 能進行加載, true: 不能被加載)

    // (1) Delegate to our parent if requested
    // 如果配置了 parent-first 模式, 那麼委託給父加載器                      // 9. 當進行加載 javax 下面的包 就直接交給 parent(sharedClassLoader) 來進行加載 (爲什麼? 主要是 這些公共加載的資源統一由 sharedClassLoader 來進行加載, 能減少 Perm 區域的大小)
    if (delegateLoad) {                                                   // 10. 若 delegate 開啓, 優先使用 parent classloader( delegate 默認是 false); 這裏還有一種可能, 就是 經過 filter(name) 後, 還是返回 true, 那說明 WebappClassLoader 不應該進行加載, 應該交給其 parent 進行加載
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        try {
            clazz = Class.forName(name, false, parent);                   // 11. 通過 parent ClassLoader 來進行加載 (這裏構造函數中第二個參數 false 表示: 使用 parent 加載 classs 時不進行初始化操作, 也就是 不會執行這個 class 中 static 裏面的初始操作 以及 一些成員變量ed賦值操作, 這一動作也符合 JVM 一貫的 lazy-init 策略)
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);                                           // 12. 通過 parent ClassLoader 加載成功, 則直接返回
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    // (2) Search local repositories
    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    try {
        // 從 WebApp 中去加載類, 主要是 WebApp 下的 classes 目錄 與 lib 目錄
        clazz = findClass(name);                                         // 13. 使用當前的 WebappClassLoader 加載
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Loading class from local repository");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
    } catch (ClassNotFoundException e) {
        // Ignore
    }

    // (3) Delegate to parent unconditionally
    // 如果在當前 WebApp 中無法加載到, 委託給 StandardClassLoader 從 $catalina_home/lib 中去加載
    if (!delegateLoad) {                                                 // 14. 這是在 delegate = false 時, 在本 classLoader 上進行加載後, 再進行操作這裏
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        try {
            clazz = Class.forName(name, false, parent);                 // 15. 用 WebappClassLoader 的 parent(ExtClassLoader) 來進行加載
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    throw new ClassNotFoundException(name);                            // 16. 若還是加載不到, 那就拋出異常吧
}

在上面步驟中, WebappClassLoader首選會在本地資源來獲取 class, 見方法 findLoadedClass0

protected Class<?> findLoadedClass0(String name) {                  // 1. 根據加載的 className 來加載 類

    String path = binaryNameToPath(name, true);                     // 2. 將 類名轉化成 類的全名稱

    ResourceEntry entry = resourceEntries.get(path);                // 3. resourceEntries 是 WebappClassLoader 加載好的 class 存放的地址
    if (entry != null) {
        return entry.loadedClass;                                   // 4. 將 加載好的 class 直接返回
    }
    return null;
}

12、WebappClassLoader findClassInternal方法

WebappClassLoader 作爲ClassLoader 的子類, 其實現了自己的一套資源查找方法, 具體的邏輯在 findClassInternal 中

protected Class<?> findClassInternal(String name)
    throws ClassNotFoundException {

    if (!validate(name))                                    // 1. 對於 J2SE 下面的 Class, 不能通過這個 WebappClassloader 來進行加載
        throw new ClassNotFoundException(name);

    String path = binaryNameToPath(name, true);             // 2. 將類名轉化成路徑名稱

    ResourceEntry entry = null;

    if (securityManager != null) {
        PrivilegedAction<ResourceEntry> dp =
            new PrivilegedFindResourceByName(name, path);
        entry = AccessController.doPrivileged(dp);
    } else {
        entry = findResourceInternal(name, path);          // 3. 調用 findResourceInternal  返回 class 的包裝類 entry
    }

    if (entry == null)
        throw new ClassNotFoundException(name);

    Class<?> clazz = entry.loadedClass;                    // 4. 若程序已經生成了 class, 則直接返回
    if (clazz != null)
        return clazz;

    synchronized (this) {
        clazz = entry.loadedClass;
        if (clazz != null)
            return clazz;

        if (entry.binaryContent == null)
            throw new ClassNotFoundException(name);

        // Looking up the package
        String packageName = null;
        int pos = name.lastIndexOf('.');
        if (pos != -1)
            packageName = name.substring(0, pos);         // 5. 獲取包名

        Package pkg = null;

        if (packageName != null) {
            pkg = getPackage(packageName);                // 6. 通過 包名 獲取對應的 Package 對象
            // Define the package (if null)
            if (pkg == null) {                            // 7. 若還不存在, 則definePackage
                try {
                    if (entry.manifest == null) {
                        definePackage(packageName, null, null, null, null,
                                null, null, null);
                    } else {
                        definePackage(packageName, entry.manifest,
                                entry.codeBase);
                    }
                } catch (IllegalArgumentException e) {
                    // Ignore: normal error due to dual definition of package
                }
                pkg = getPackage(packageName);            // 8. 獲取 Package
            }
        }

        if (securityManager != null) {                    // 9. 若程序運行配置了 securityManager, 則進行一些權限方面的檢查

            // Checking sealing
            if (pkg != null) {
                boolean sealCheck = true;
                if (pkg.isSealed()) {
                    sealCheck = pkg.isSealed(entry.codeBase);
                } else {
                    sealCheck = (entry.manifest == null)
                        || !isPackageSealed(packageName, entry.manifest);
                }
                if (!sealCheck)
                    throw new SecurityException
                        ("Sealing violation loading " + name + " : Package "
                         + packageName + " is sealed.");
            }

        }

        try {                                            // 10 最終調用 ClassLoader.defineClass 來將 class 對應的 二進制數據加載進來, 進行 "加載, 連接(解析, 驗證, 準備), 初始化" 操作, 最終返回 class 對象
            clazz = defineClass(name, entry.binaryContent, 0,                       
                    entry.binaryContent.length,
                    new CodeSource(entry.codeBase, entry.certificates));
        } catch (UnsupportedClassVersionError ucve) {
            throw new UnsupportedClassVersionError(
                    ucve.getLocalizedMessage() + " " +
                    sm.getString("webappClassLoader.wrongVersion",
                            name));
        }
        // Now the class has been defined, clear the elements of the local
        // resource cache that are no longer required.
        entry.loadedClass = clazz;
        entry.binaryContent = null;
        entry.codeBase = null;
        entry.manifest = null;
        entry.certificates = null;
        // Retain entry.source in case of a getResourceAsStream() call on
        // the class file after the class has been defined.
    }

    return clazz;                                         // 11. return 加載了的 clazz
}

13、 WebappClassLoader findResourceInternal方法

在Tomcat中, 其資源的查找都是通過 JNDI(具體存儲在了 StandardRoot裏面), WebappClassLoader 的資源查找, 並且將找到的資源轉化成 byte[] 就是在 findResourceInternal 裏面實現

protected ResourceEntry findResourceInternal(final String name, final String path) {

    if (!started) {
        log.info(sm.getString("webappClassLoader.stopped", name));
        return null;
    }

    if ((name == null) || (path == null))
        return null;

    ResourceEntry entry = resourceEntries.get(path);        // 1. resourceEntries 裏面會存儲所有已經加載了的 文件的信息
    if (entry != null)
        return entry;

    boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX);

    WebResource resource = null;

    boolean fileNeedConvert = false;

    resource = resources.getClassLoaderResource(path);      // 2. 通過 JNDI 來進行查找 資源 (想知道 resources 裏面到底是哪些資源, 可以看 StandardRoot 類)

    if (!resource.exists()) {                               // 3. 若資源不存在, 則進行返回
        return null;
    }

    entry = new ResourceEntry();                            // 4. 若所查找的 class 對應的 ResourceEntry 不存在, 則進行構建一個
    entry.source = resource.getURL();
    entry.codeBase = entry.source;
    entry.lastModified = resource.getLastModified();

    if (needConvert) {
        if (path.endsWith(".properties")) {
            fileNeedConvert = true;
        }
    }

    /* Only cache the binary content if there is some content
     * available and either:
     * a) It is a class file since the binary content is only cached
     *    until the class has been loaded
     *    or
     * b) The file needs conversion to address encoding issues (see
     *    below)
     *
     * In all other cases do not cache the content to prevent
     * excessive memory usage if large resources are present (see
     * https://issues.apache.org/bugzilla/show_bug.cgi?id=53081).
     */
    if (isClassResource || fileNeedConvert) {                               // 5. 獲取對應資源的二進制字節流, 當需要進行轉碼時, 進行相應的轉碼操作
        byte[] binaryContent = resource.getContent();
        if (binaryContent != null) {
             if (fileNeedConvert) {
                // Workaround for certain files on platforms that use
                // EBCDIC encoding, when they are read through FileInputStream.
                // See commit message of rev.303915 for details
                // http://svn.apache.org/viewvc?view=revision&revision=303915
                String str = new String(binaryContent);
                try {
                    binaryContent = str.getBytes(StandardCharsets.UTF_8);   // 6. 進行資源轉碼爲 UTF-8
                } catch (Exception e) {
                    return null;
                }
            }
            entry.binaryContent = binaryContent;                           // 7. 獲取資源對應的 二進制數據信息
            // The certificates and manifest are made available as a side
            // effect of reading the binary content
            entry.certificates = resource.getCertificates();               // 8. 獲取資源的證書
        }
    }
    entry.manifest = resource.getManifest();

    if (isClassResource && entry.binaryContent != null &&
            this.transformers.size() > 0) {
        // If the resource is a class just being loaded, decorate it
        // with any attached transformers
        String className = name.endsWith(CLASS_FILE_SUFFIX) ?
                name.substring(0, name.length() - CLASS_FILE_SUFFIX.length()) : name;
        String internalName = className.replace(".", "/");

        for (ClassFileTransformer transformer : this.transformers) {
            try {
                byte[] transformed = transformer.transform(
                        this, internalName, null, null, entry.binaryContent
                );
                if (transformed != null) {
                    // 設置 二進制設置到 ResourceEntry
                    entry.binaryContent = transformed;
                }
            } catch (IllegalClassFormatException e) {
                log.error(sm.getString("webappClassLoader.transformError", name), e);
                return null;
            }
        }
    }

    // Add the entry in the local resource repository
    synchronized (resourceEntries) {                                        // 9. 將生成的 entry 放入 resourceEntries 中
        // Ensures that all the threads which may be in a race to load
        // a particular class all end up with the same ResourceEntry
        // instance
        ResourceEntry entry2 = resourceEntries.get(path);
        if (entry2 == null) {
            // 向本地資源緩存註冊 ResourceEntry
            resourceEntries.put(path, entry);
        } else {
            entry = entry2;
        }
    }

    return entry;
}

14、 WebappClassLoader stop 方法

在進行熱部署/重部署時, 會調用 WebappClassLoader 的 stop 方法, 它主要做了下面四種資源的清除

public void stop() throws LifecycleException {

    // Clearing references should be done before setting started to
    // false, due to possible side effects
    clearReferences();              // 1. 清除各種資源

    started = false;

    resourceEntries.clear();        // 2. 清空各種 WebappClassLoader 加載的數據
    jarModificationTimes.clear();   // 3. 清空各種 監視的資源(監視的資源一旦有變動, 就會觸發 StandardContext 的重新加載機制)
    resources = null;

    permissionList.clear();         // 4. 下面兩個清空的是與 Java 權限相關的資源
    loaderPC.clear();
}

15、總結

現在再回頭看看開篇提出的問題, 現在我們有了答案了, 先看 Tomcat classLoader 設計的優點吧!

1. 熱部署功能或項目(PS: 熱部署JSP, Context)
2. 隔離資源的訪問
    (1) 不同的 Context 之間不能相互訪問對方加載的資源, 舉例: 可能Context1用Spring3.1, 而 Context2用Spring4.1 若用同一個Classloader 則遇到 spring 的class只能加載一份, 就會出現想用 spring4.1裏面的 AnnotationUtils, 但是 classLoader 其實加載的是 spring 3.1裏面的類, 這樣很有可能出現 NoSuchMethodError 異常
    (2) 不讓 Context 加載類不能訪問到 Tomcat 容器自身的類

但我們再想想, 爲了一個熱部署, Tomcat 在Stop方法裏面做了多少的清理工作, 而在真實產線上 很少用Tomcat的reload, 爲啥? 就是我們寫的程序有時會做些Tomcat始料不及的事情 (比如 自己創建一些ClassLoader, 再用這個 ClassLoader 開啓一個 loop, loop裏面有引用 WebappClassLoader 加載出來的數據, 想想就覺得害怕....), 這樣的話 Tomcat, 就不能完全清理所有的資源, 最終在 幾次 StandardContext.reload 的情況下, Tomcat最終因爲內存溢出而掛了!

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