Tomcat - 模擬Tomcat的webappClassLoader加載自己war包應用內不同版本類實現相互共存與隔離


在這裏插入圖片描述

Pre

Tomcat - 都說Tomcat違背了雙親委派機制,到底對不對?

JVM-白話聊一聊JVM類加載和雙親委派機制源碼解析

JVM - 實現自定義的ClassLoader就是這麼簡單


Tomcat要解決什麼問題?

作爲一個Web容器,Tomcat要解決什麼問題 , Tomcat 如果使用默認的雙親委派類加載機制能不能行?

  1. 我們知道Tomcat可以部署多個應用,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離 .

    舉個例子 假設APP1 使用的是 Spring4 , APP2 使用的是Spring5 , 毫無疑問 Spring4 和 Spring 5 肯定有 類的全路徑一樣的類吧,如果使用雙親委派 ,父加載器加載誰?

  2. 部署在同一個web容器中相同的類庫相同的版本可以共享, 比如jdk的核心jar包,否則,如果服務器有n個應用程序,那麼要有n份相同的類庫加載進虛擬機。

  3. web容器 自己依賴的類庫 (tomcat lib目錄下),不能與應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
    在這裏插入圖片描述

  4. web容器要支持jsp的修改, jsp 文件最終也是要編譯成class文件才能在虛擬機中運行, web容器需要支持 jsp 修改後不用重啓 ,就是熱加載的功能。

結合上面的4個問題,我們看下雙親委派能不能支持?

第一個問題,如果使用默認的類加載器機制,肯定是無法加載兩個相同類庫的不同版本的,如果使用雙親委派,讓父加載器去加載 ,不管你是什麼版本的,只要你的全限定類名一樣,那肯定只有一份,APP 隔離 無法滿足

第二個問題,默認的類加載器是能夠實現的,很好理解嘛, 就是雙親委派的功能,保證唯一性。

第三個問題和第一個問題一樣。

第四個問題, 要怎麼實現jsp文件的熱加載呢? jsp 文件其實也就是class文件,那麼如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改後的jsp是不會重新加載的。那麼怎麼辦呢?可以直接卸載掉這jsp文件的類加載器 .當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。 源碼詳見: org.apache.jasper.servlet.JasperLoader


Tomcat違反了雙親委派機制?

      Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

也不盡然,核心的Java的加載還是遵從雙親委派 。

Tomcat中 各個web應用自己的類加載器(WebAppClassLoader)會優先加載,打破了雙親委派機制。加載不到時再交給commonClassLoader走雙親委託 .


模擬Tomcat的webappClassLoader加載自己war包應用內不同版本類實現相互共存與隔離

我們基於JVM - 實現自定義的ClassLoader就是這麼簡單

package com.gof.facadePattern;

import java.io.FileInputStream;
import java.lang.reflect.Method;

/**
 * @author 小工匠
 * @version v1.0
 * @create 2020-06-11 23:09
 * @motto show me the code ,change the word
 * @blog https://artisan.blog.csdn.net/
 * @description
 **/

public class MyClassLoaderTest {

    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }



        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    if ("com.gof.facadePattern.Boss1".equals(name)){
                        c = findClass(name);
                    }else{
                        // 交由父加載器去加載
                        c = this.getParent().loadClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                //defineClass將一個字節數組轉爲Class對象,這個字節數組是class文件讀取後最終的字節數組。
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    }

    public static void main(String args[]) throws Exception {
        //初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載器設置爲應用程序類加載器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/artisan");
        //D盤創建 artisan/com/gof/facadePattern 目錄,將Boss類的複製類Boss1.class丟入該目錄
        Class clazz = classLoader.loadClass("com.gof.facadePattern.Boss1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader() );

        System.out.println();


        MyClassLoader classLoader1 = new MyClassLoader("D:/artisan1");
        //D盤創建 artisan1/com/gof/facadePattern 目錄,將Boss類的複製類Boss1.class丟入該目錄
        Class clazz1 = classLoader1.loadClass("com.gof.facadePattern.Boss1");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("sout", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader() );

    }
}

在這裏插入圖片描述
爲了好區分 我們把Boss1 的類 ,sout方法的輸出稍微調整下,以示區別。

應用中的Boss1 無需刪除

在這裏插入圖片描述

同時模擬第二個應用, 在D盤創建 artisan1/com/gof/facadePattern 目錄,將Boss類的複製類Boss1.class丟入該目錄

基於以上前置條件,得出如下結論

我們通過上面的示例模擬出了同一個JVM內, 分別使用不同的類加載器(new 出來的)去加載不同classpath下的類,而避免了走雙親委派,去模擬tomcat的類加載機制

通過結論可以得出在同一個JVM內,兩個相同包名和類名的類對象可以共存,是因爲他們的類加載器不一樣。

所以看兩個類對象是否是同一個,除了看類的包名和類名是否都相同之外,還需要他們的類加載器是否相同


Tomcat加載機制小結

在這裏插入圖片描述

當tomcat啓動時,會創建幾種類加載器:

  1. Bootstrap 引導類加載器 : 加載JVM啓動所需的類,以及標準擴展類(位於jre/lib/ext下)

  2. System 系統類加載器 : 加載tomcat啓動的類,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位於CATALINA_HOME/bin

在這裏插入圖片描述
4. webapp 應用類加載器: 每個應用在部署後,都會創建一個唯一的類加載器。該類加載器會加載位於 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

  1. Common 通用類加載器:加載tomcat使用以及應用通用的一些類,位於CATALINA_HOME/lib下,比如servlet-api.jar

在這裏插入圖片描述


總之 當應用需要到某個類時,則會按照下面的順序進行類加載:

1 使用bootstrap引導類加載器加載 (JVM 的東西 )

2 使用system系統類加載器加載 (tomcat的啓動類Bootstrap包)

3 使用WebAppClassLoader 加載 WEB-INF/classes (應用自定義的class)

4 使用WebAppClassLoader 加載在WEB-INF/lib (應用的依賴包)

5 使用common類加載器在CATALINA_HOME/lib中加載 (tomcat的依賴包,公共的,被各個應用共享的)


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