天天看點

"類加載器"與"雙親委派機制"一網打盡

作者:小牛呼噜噜

引子

大家好,我是呼噜噜,大家想必都有過平時開發springboot 項目的時候稍微改動一點代碼,就得重新開機,就很煩

網上一般介紹 2種方式spring-boot-devtools,或者通過JRebel插件 來實作"熱部署"

熱部署就是當應用正在運作時,修改應用不需要重新開機應用。

其中 spring-boot-devtools其實是自動重新開機,主要是節省了我們手動點選重新開機的時間,不算真正意義上的熱部署。JRebel插件啥都好,就是需要收費

"類加載器"與"雙親委派機制"一網打盡

但如果平時我們在調試debug的情況下,隻是在方法塊内代碼修改了一下,我們還得重新開機項目,就很浪費時間。這個時候我們其實可以直接build ,不重新開機項目,即可 實作熱部署。

我們先來寫一個例子示範一下:

@RestController
public class TestController {
    @RequestMapping(value = "/test",method = {RequestMethod.GET, RequestMethod.POST})
    public void testclass() {
        String name = "zj";
        int weight = 100;
        System.out.println("name:"+ name);
        System.out.println("weight: "+weight);
    }
}           

結果:

name:zj weight: 100

修改代碼,然後直接build項目,不重新開機項目,我們再請求這個測試接口:

String name = "ming";
int weight = 300;           

神奇的一幕出現了,結果為:

name:ming weight: 300

當我們修改.java檔案,隻需重新生成對應的.class檔案,就能影響到程式運作結果, 無需重新開機,Why? 背後JVM的操作原理且看本文娓娓道來。

了解.class檔案

首先我們得先了解一下 什麼是.class檔案

舉個簡單的例子,建立一個Person類:

public class Person {
    /**
     * 狀态 or 屬性
     */
    String name;//姓名
    String sex;//性别
    int height;//身高
    int weight;//體重
    
    /**
     * 行為
     */
    public void sleep(){
        System.out.println(this.name+"--"+ "睡覺");
    }
    public void eat(){
        System.out.println("吃飯");
    }
    public void Dance(){
        System.out.println("跳舞");
    }
}           

我們執行javac指令,生成Person.class檔案

然後我們通過vim 16進制打開它

#打開file檔案
vim Person.class 

#在指令模式下輸入.. 以16進制顯示
 :%!xxd
 
#在指令模式下輸入.. 切換回預設顯示
:%!xxd -r           
"類加載器"與"雙親委派機制"一網打盡

不同的作業系統,不同的 CPU 具有不同的指令集,JAVA能做到平台無關性,依靠的就是 Java 虛拟機。 .java源碼是給人類讀的,而.class位元組碼是給JVM虛拟機讀的,計算機隻能識别 0 和 1組成的二進制檔案,是以虛拟機就是我們編寫的代碼和計算機之間的橋梁。

虛拟機将我們編寫的 .java 源程式檔案編譯為 位元組碼 格式的 .class 檔案,位元組碼是各種虛拟機與所有平台統一使用的程式存儲格式,class檔案主要用于解決平台無關性的中間檔案

"類加載器"與"雙親委派機制"一網打盡

類加載的過程

在之前的一篇文章談談JAVA中對象和類、this、super和static關鍵字中,我們知曉 Java 是如何建立對象的

Person zhang = new Person();           

雖然我們寫的時候是簡單的一句,但是JVM内部的實作過程卻是複雜的:

将硬碟上指定位置的Person.class檔案加載進記憶體

執行main方法時,在棧記憶體中開辟了main方法的空間(壓棧-進棧),然後在main方法的棧區配置設定了一個變量zhang。

執行new,在堆記憶體中開辟一個 實體類的 空間,配置設定了一個記憶體首位址值

調用該實體類對應的構造函數,進行初始化(如果沒有構造函數,Java會補上一個預設構造函數)。

将實體類的 首位址指派給zhang,變量zhang就引用了該實體。(指向了該對象)

"類加載器"與"雙親委派機制"一網打盡

其中 上圖步驟1 Classloader(類加載器) 将class檔案加載到記憶體中具體分為3個步驟:加載、連接配接、初始化

類的生命周期一般有如下圖有7個階段,其中階段1-5為類加載過程,驗證、準備、解析統稱為連接配接

"類加載器"與"雙親委派機制"一網打盡
  1. 加載

加載階段:指的是将類對應的.class檔案中的二進制位元組流讀入到記憶體中,将這個位元組流轉化為方法區的運作時資料結構,然後在堆區建立一個java.lang.Class 對象,作為對方法區中這些資料的通路入口

相對于類加載的其他階段而言,加載階段(準确地說,是加載階段擷取類的二進制位元組流的動作)是我們最可以控制的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義類加載器來完成加載。這個我們文章後面再詳細講

  1. 驗證

驗證階段:校驗位元組碼檔案正确性。這一階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。

這部分對開發者而言是無法幹預的,以下内容了解即可

更多精彩文章在公衆号「小牛呼噜噜」

驗證階段大緻會完成4個階段的檢驗動作:

檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以0xCAFEBABE開頭、主次版本号是否在目前虛拟機的處理範圍之内、常量池中的常量是否有不被支援的類型。 中繼資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。 位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。 符号引用驗證:確定解析動作能正确執行。 驗證階段是非常重要的,但不是必須的,它對程式運作期沒有影響,如果所引用的類經過反複驗證,那麼可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間。
  1. 準備

準備階段:為類變量(static 修飾的變量)配置設定記憶體,并将其初始化為預設值 注意此階段僅僅是為類變量 即靜态變量配置設定記憶體,并将其初始化為預設值 舉個例子,在這個準備階段:

static int value = 3;//類變量 初始化,設為預設值 0,不是 3哦 !!!

int num = 4;//類成員變量,在這個階段不初始化;在 new類,調用對應類的構造函數才進行初始化

final static valFin = 5;//這個比較特殊,在這個階段也不會配置設定記憶體!!!           

注意: valFin是被final static修飾的常量在 編譯 的時候已配置設定好了,是以在準備階段 此時的值為5,是以在這個階段也不會初始化!

  1. 解析

解析階段: 是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。

符号引用就是一組符号來描述目标,可以是任何字面量。 直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄。

這個階段了解一下即可

  1. 初始化

直到初始化階段,Java虛拟機才真正開始執行類中編寫的Java程式代碼,将主導權移交給應用程式。

初始化階段 是類加載過程的最後一個步驟,之前介紹的幾個類加載的動作裡,除了在加載階段使用者應用程式可以通過自定義類加載器的方式局部參與外,其餘動作都完全由Java虛拟機來主導控 制。 Java程式對類的使用方式可分為兩種:主動使用與被動使用。一般來說隻有當對類的首次主動使用的時候才會導緻類的初始化,是以主動使用又叫做類加載過程中“初始化”開始的時機。

類執行個體初始化方式,主要是以下幾種:

1、建立類的執行個體,也就是new的方式 2、通路某個類或接口的靜态變量,或者對該靜态變量指派 3、調用類的靜态方法 4、反射(如Class.forName("com.test.Person")) 5、初始化某個類的子類,則其父類也會被初始化 6、Java虛拟機啟動時被标明為啟動類的類(JavaTest),還有就是Main方法的類會 首先被初始化

這邊就不展開說了,大家記住即可

  1. 使用

當JVM完成初始化階段之後,JVM便開始從入口方法開始執行使用者的程式代碼

  1. 解除安裝

當使用者程式代碼執行完畢後,JVM便開始銷毀建立的Class對象,最後負責運作的JVM也退出記憶體 在如下幾種情況下,Java虛拟機将結束生命周期

執行了System.exit()方法 程式正常執行結束 程式在執行過程中遇到了異常或錯誤而異常終止 由于作業系統出現錯誤而導緻Java虛拟機程序終止

類加載器 與 雙親委派機制

上文類加載過程中,是需要類加載器的參與,類加載器在Java中非常重要,它使得 Java 類可以被動态加載到 Java 虛拟機中并執行

那什麼是類加載器?通過一個類的全限定名來擷取描述此類的二進制位元組流到JVM中,然後轉換為一個與目标類對應的java.lang.Class對象執行個體

Java虛拟機支援類加載器的種類:主要包括3中: 引導類加載器(Bootstrap ClassLoader)、擴充類加載器(Extension ClassLoader)、應用類加載器(系統類加載器,AppClassLoader),另外我們還可以自定義加載器-使用者自定義類加載器

"類加載器"與"雙親委派機制"一網打盡
  1. 引導類加載器(Bootstrap ClassLoader):BootStrapClassLoader是由c++實作的。引導類加載器加載java運作過程中的核心類庫JRE\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放 在JRE\classes裡的類,也就是JDK提供的類等常見的比如:Object、Stirng、List等
  2. 擴充類加載器(Extension ClassLoader):它用來加載/jre/lib/ext目錄以及java.ext.dirs系統變量指定的類路徑下的類。
  3. 應用類加載器(AppClassLoader):它主要加載應用程式ClassPath下的類(包含jar包中的類)。它是java應用程式預設的類加載器。其實就是加載我們一般開發使用的類
  4. 使用者自定義類加載器: 使用者根據自定義需求,自由的定制加載的邏輯,隻需繼承應用類加載器AppClassLoader,負責加載使用者自定義路徑下的class位元組碼檔案
  5. 線程上下文類加載器:除了以上列舉的三種類加載器,其實還有一種比較特殊的類型就是線程上下文類加載器。ThreadContextClassLoader可以是上述類加載器的任意一種,這個我們下文再細說

我們來看一個例子:

public class TestClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = TestClassLoader.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());//擷取其父類加載器
        System.out.println(classLoader.getParent().getParent());//擷取父類的父類加載器
    }
}           

結果:

sun.misc.Launcher

ExtClassLoader@5caf905d null

結果顯示分别列印應用類加載器、擴充類加載器和引導類加載器 由于 引導類加載器 是由c++實作的,是以并不存在一個Java的類,是以會列印出null 我們還可以看到結果裡面列印了 sun.misc.Launcher,這個是什麼東東?

其實Launcher是JRE中用于啟動程式入口main()的類,我們看下Launcher的源碼:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader(); //加載擴充類類加載器
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);//加載應用程式類加載器,并設定parent為extClassLoader
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        Thread.currentThread().setContextClassLoader(loader); //設定AppClassLoader為線程上下文類加載器
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}           

其中loader = AppClassLoader.getAppClassLoader(extcl);的核心方法源碼如下:

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;//設定parent
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
    }           

通過以上源碼我們可以知曉:

  1. Launcher的ClassLoader是BootstrapClassLoader,在Launcher建立的同時,還會同時建立ExtClassLoader,AppClassLoader(并設定其parent為extClassLoader)。其中代碼中 "sun.boot.class.path"是BootstrapClassLoader加載的jar包路徑。
  2. 這幾種類加載器 都遵循 雙親委派機制

雙親委派機制說的其實就是,當一個類加載器收到一個類加載請求時,會去判斷有沒有加載過,如果加載過直接傳回,否則該類加載器會把請求先委派給父類加載器。每個類加載器都是如此,隻有在父類加載器在自己的搜尋範圍内找不到指定類時,子類加載器才會嘗試自己去加載。

更多精彩文章在公衆号「小牛呼噜噜」

雙親委派模式優勢:

  1. 避免類的重複加載, 當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次, 這樣保證了每個類隻被加載一次。
  2. 保護程式安全,防止核心API被随意篡改,比如 java核心api中定義類型不會被随意替換

我們這裡看一個例子:

我們建立一個自己的類“String”放在src/java/lang目錄下

public class String {
    static {
        System.out.println("自定義 String類");
    }
}           

建立StringTest類:

public class StringTest {
    public static void main(String[] args) {
        String str=new java.lang.String();
        System.out.println("start test-------");
    }
}           

結果:

start test-------

可以看出,程式并沒有運作我們自定義的“String”類,而是直接傳回了String.class。像String,Integer等類 是JAVA中的核心類,是不允許随意篡改的!

ClassLoader

ClassLoader 是一個抽象類,負責加載類,像 ExtClassLoader,AppClassLoader 都是由該類派生出來,實作不同的類裝載機制。這塊的源碼太多了,就不貼了。

我們來看下 它的核心方法loadClass(),傳入需要加載的類名,它會幫你加載:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 一開始先 檢查是否已經加載該類
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果未加載過類,則遵循 雙親委派機制,來加載類
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                //如果父類是null就是BootstrapClassLoader,使用 啟動類類加載器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                long t1 = System.nanoTime();
                // 如果還是沒有加載成功,調用findClass(),讓目前類加載器加載
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

// 繼承的子類得重寫該方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}           

loadClass()源碼 展示了,一般加載.class檔案大緻流程:

  1. 先去緩存中 檢查是否已經加載該類,有就直接傳回,避免重複加載;沒有就下一步
  2. 遵循 雙親委派機制,來加載.class檔案
  3. 上面兩步都失敗了,調用findClass()方法,讓目前類加載器加載

注意:由于ClassLoader類是抽象類,而抽象類是無法通過new建立對象的,是以它最核心的findClass()方法,沒有具體實作,隻抛了一個異常,而且是protected的,這是應用了模闆方法模式,具體的findClass()方法丢給子類實作, 是以繼承的子類得重寫該方法。

自定義類加載器

編寫一個自定義的類加載器

那我們仿照ExtClassLoader,AppClassLoader 來實作一個自定義的類加載器,我們同樣是繼承ClassLoader類

編寫一個測試類TestPerson

public class TestPerson {
    String name = "xiao ming";
    public void print(){
        System.out.println("hello my name is: "+ name);
    }
}           

接着 編寫一個自定義類加載器MyTestClassLoader:

public class MyTestClassLoader extends ClassLoader  {

    final String classNameSpecify  = "TestPerson";

    public MyTestClassLoader() {

    }


    public MyTestClassLoader(ClassLoader parent)
    {
        super(parent);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private File getClassFile(String name)
    {
        File file = new File("D:\\ideaProjects\\src\\main\\java\\com\\zj\\ideaprojects\\test2\\"+ classNameSpecify+ ".class");
        return file;
    }

    private byte[] getClassBytes(File file) throws Exception
    {
        // 這裡要讀入.class的位元組,是以要使用位元組流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }

    //我們這邊要打破雙親委派模型,重寫整個loadClass方法
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null && name.contains(classNameSpecify)){//指定的類,不走雙親委派機制,自定義加載
            c = findClass(name);
            if (c != null){
                return c;
            }
        }
        return super.loadClass(name);
    }
}           

最後在編寫一個測試controller:

@RestController
public class TestClassController {
    @RequestMapping(value = "testClass",method = {RequestMethod.GET, RequestMethod.POST})
    public void testClassLoader() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        MyTestClassLoader myTestClassLoader = new MyTestClassLoader();
        Class<?> c1 = Class.forName("com.zj.ideaprojects.test2.TestPerson", true, myTestClassLoader);
        Object obj = c1.newInstance();
        System.out.println("目前類加載器:"+obj.getClass().getClassLoader());
        obj.getClass().getMethod("print").invoke(obj);

    }
}           

先找到TestPerson所在的目錄, 執行指令: javac TestPerson,生成TestPerson.class

這裡沒有使用idea的build,是因為我們代碼的class讀取路徑 是寫死了的,不走預設CLASSPATH

D:\ideaProjects\src\main\java\com\zj\ideaprojects\test2\TestPerson.class

我們然後用postman調用testClassLoader()測試接口

結果:

目前類加載器:com.zj.ideaprojects.test2.MyTestClassLoader@1d75e392 hello my name is: xiao ming

然後修改TestPerson,将name 改為 “xiao niu”

public class TestPerson {
    String name = "xiao niu";
    public void print(){
        System.out.println("hello my name is: "+ name);
    }
}           

然後在目前目錄 重新編譯, 執行指令: javac TestPerson,會在目前目錄重新生成TestPerson.class 不重新開機項目,直接用postman 直接調這個測試接口 結果:

目前類加載器:com.zj.ideaprojects.test2.MyTestClassLoader@7091bd27 hello my name is: xiao niu

這樣就實作了“熱部署”!!!

"類加載器"與"雙親委派機制"一網打盡

為什麼我們這邊要打破雙親委派機制?

如果不打破的話,結果 目前類加載器會顯示"sun.misc.Launcher$AppClassLoader",原因是由于idea啟動項目的時候會自動幫我們編譯,将class放到 CLASSPATH路徑下。其實可以把預設路徑下的.class删除也行。這裡也是為了展示如何打破雙親委派機制,才如此實作的。

官方推薦我們自定義類加載器時,遵循雙親委派機制。但是凡事得看實際需求嘛

"類加載器"與"雙親委派機制"一網打盡

自定義類加載器時,如何打破雙親委派機制

通過上面的例子我們可以看出: 1、如果不想打破雙親委派機制,我們自定義類加載器,那麼隻需要重寫findClass方法即可 2、如果想打破雙親委派機制,我們自定義類加載器,那麼還得重寫整個loadClass方法

SPI機制 與 線程上下文類加載器

如果你閱讀到這裡,你會發現雙親委派機制的各種好處,但萬物都不是絕對正确的,我們需要一分為二地看待問題。

在某些場景下雙親委派制過于局限,是以有時候必須打破雙親委派機制來達到目的。比如 :SPI機制、線程上下文類加載器

  1. SPI(Service Provider Interface)服務提供接口。它是jdk内置的一種服務發現機制,将裝配的控制權移到程式之外,在子產品化設計中這個機制尤其重要,其核心思想就是 讓服務定義與實作分離、解耦。
"類加載器"與"雙親委派機制"一網打盡
  1. 線程上下文類加載器(context class loader)是可以破壞Java類加載委托機制,使程式可以逆向使用類加載器,使得java類加載體系顯得更靈活。

Java 應用運作的初始線程的上下文類加載器是應用類加載器,線上程中運作的代碼可以通過此類加載器來加載類和資源。Java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來擷取和設定線程的上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)方法進行設定的話,線程将繼承其父線程的上下文類加載器。

更多精彩文章在公衆号「小牛呼噜噜」

SPI機制在架構的設計上應用廣泛,下面舉幾個常用的例子:

JDBC

平時擷取jdbc,我們可以這樣: Connection connection =DriverManager.getConnection("jdbc://localhost:3306");

我們讀DriverManager的源碼發現:其實就是查詢classPath下,所有META-INF下給定Class名的檔案,并将其内容傳回,使用疊代器周遊,這裡周遊的内部使用Class.forName加載了類。

其中有一處非常重要ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);我們看下它的實作:

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();//important !
        return ServiceLoader.load(service, cl);
    }           

我們可以看出JDBC,DriverManager類和ServiceLoader類都是屬于核心庫 rt.jar 的,它們的類加載器是Bootstrap ClassLoader類加載器。而具體的資料庫驅動相關功能卻是第三方提供的,第三方的類不能被引導類加載器(Bootstrap ClassLoader)加載。

是以java.util.ServiceLoader類進行動态裝載時,使用了線程的上下文類加載器(ThreadContextClassLoader)讓父級類加載器能通過調用子級類加載器來加載類,這打破了雙親委派機制。

Tomcat

Tomcat是web容器,我們把war包放到 tomcat 的webapp目錄下,這意味着一個tomcat可以部署多個應用程式。

不同的應用程式可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。防止出現一個應用中加載的類庫會影響另一個應用的情況。如果采用預設的雙親委派類加載機制,那麼是無法加載多個相同的類。

"類加載器"與"雙親委派機制"一網打盡
  1. 如果Tomcat本身的依賴和Web應用還需要共享,Common類加載器(CommonClassLoader)來裝載實作共享
  2. Catalina類加載器(CatalinaClassLoader) 用來 隔絕Web應用程式與Tomcat本身的類
  3. Shared類加載器(SharedClassLoader):如果WebAppClassLoader自身沒有加載到某個類,那就委托SharedClassLoader去加載
  4. WebAppClassLoader: 為了實作隔離性,優先加載 Web 應用自己定義的類,是以沒有遵照雙親委派的約定,每一個應用自己的類加載器WebAppClassLoader(多個應用程式,就有多個WebAppClassLoader),負責優先加載本身的目錄下的class檔案,加載不到時再交給CommonClassLoader以及上層的ClassLoader進行加載,這破壞了雙親委派機制。
  5. Jsp類加載器(JasperLoader):實作熱部署的功能,修改檔案不用重新開機就自動重新裝載類庫。JasperLoader的加載範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丢棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的執行個體,并通過再建立一個新的Jsp類加載器來實作JSP檔案的HotSwap功能。

我們來模拟一下tomcat 多個版本代碼共存:

這邊的例子換了個電腦,是以目錄結構、路徑與上面的例子有點變化
"類加載器"與"雙親委派機制"一網打盡

我們先編寫 App類

public class App {
    String name = "webapp 1";
    public void print() {
        System.out.println("this is "+ name);
    }
}           

javac App生成的App.class 放入 tomcatTest\war1\com\zj\demotest\tomcatTest 目錄下

然後将name改為webapp 2,重新生成的App.class 放入 tomcatTest\war2\com\zj\demotest\tomcatTest 目錄下

更多精彩文章在公衆号「小牛呼噜噜」

然後我們編寫類加載器:

public class MyTomcatClassloader extends ClassLoader {

    private String classPath;

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


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private File getClassFile(String name)
    {
        name = name.replaceAll("\\.", "/");
        File file = new File(classPath+ "/"+ name + ".class");//拼接路徑,找到class檔案
        return file;
    }

    private byte[] getClassBytes(File file) throws Exception
    {
        // 這裡要讀入.class的位元組,是以要使用位元組流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1) {
                break;
            }

            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }

    //我們這邊要打破雙親委派模型,重寫整個loadClass方法
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null && name.contains("tomcatTest")){//指定的目錄下的類,不走雙親委派機制,自定義加載
            c = findClass(name);
            if (c != null){
                return c;
            }
        }
        return super.loadClass(name);
    }

}           

最後編寫測試controller:

@RestController
public class TestController {

    @RequestMapping(value = "/testTomcat",method = {RequestMethod.GET, RequestMethod.POST})
    public void testclass() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyTomcatClassloader myTomcatClassloader = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war1");
        Class cl = myTomcatClassloader.loadClass("com.zj.demotest.tomcatTest.App");
        Object obj = cl.newInstance();
        System.out.println("目前類加載器:"+obj.getClass().getClassLoader());
        obj.getClass().getMethod("print").invoke(obj);

        MyTomcatClassloader myTomcatClassloader22 = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war2");
        Class cl22 = myTomcatClassloader22.loadClass("com.zj.demotest.tomcatTest.App");
        Object obj22 = cl22.newInstance();
        System.out.println("目前類加載器:"+obj22.getClass().getClassLoader());
        obj22.getClass().getMethod("print").invoke(obj22);

    }

}           

然後postman 調一下這個接口, 結果:

目前類加載器:com.zj.demotest.tomcatTest.MyTomcatClassloader@18fbb876 this is webapp 1 目前類加載器:com.zj.demotest.tomcatTest.MyTomcatClassloader@5f7ed4a9 this is webapp 2

我們發現2個同樣的類能共存在同一個JVM中,互不影響。

注意: 同一個JVM内,2個相同的包名和類名的對象是可以共存的,前提是他們的類加載器不一樣。是以我們要判斷多個類對象是否是同一個,除了要看包名和類名相同,還得注意他們的類加載器是否一緻

SpringBoot Starter

springboot自動配置的原因是因為使用了@EnableAutoConfiguration注解。

當程式包含了EnableAutoConfiguration注解,那麼就會執行下面的方法,然後會加載所有spring.factories檔案,将其内容封裝成一個map,spring.factories其實就是一個名字特殊的properties檔案。

在spring-boot應用啟動時,會調用loadFactoryNames方法,其中傳遞的一個參數就是:org.springframework.boot.autoconfigure.EnableAutoConfiguration

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }           

META-INF/spring.factories會被讀取到。

"類加載器"與"雙親委派機制"一網打盡

它還使用了this.getBeanClassLoader() 擷取類加載器。是以我們立刻明白了文章一開始的例子,SpringBoot項目直接build項目,不重新開機項目,就能實作熱部署效果。

尾語

類加載器是 Java 語言的一個創新,它使得動态安裝和更新軟體元件成為可能。同時我們應該了解雙親委派機制的優缺點和應用場景,這些可能比較難但對于我們來說卻很重要。

本篇文章到這裡就結束啦,很感謝靓仔你能看到最後,如果覺得文章對你有幫助,别忘記關注我!更多精彩的文章

計算機内功、JAVA源碼、職業成長、項目實戰、面試相關資料等更多精彩文章在公衆号「小牛呼噜噜」

參考資料:

《深入了解Java虛拟機:JVM進階特性與最佳實踐》

https://www.cnblogs.com/keyi/p/7203170.html

https://www.cnblogs.com/szlbm/p/5504631.html

繼續閱讀