詳解Java類加載器與雙親委派機制

引子

大傢想必都有過平時開發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 對象,作為對方法區中這些數據的訪問入口

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

2.驗證

驗證階段:校驗字節碼文件正確性。這一階段的目的是為瞭確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

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

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

文件格式驗證:驗證字節流是否符合Class文件格式的規范;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型。

元數據驗證:對字節碼描述的信息進行語義分析(註意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規范的要求;例如:這個類是否有父類,除瞭java.lang.Object之外。

字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那麼可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

3.準備

準備階段:為類變量(static 修飾的變量)分配內存,並將其初始化為默認值

註意此階段僅僅是為類變量 即靜態變量分配內存,並將其初始化為默認值

舉個例子,在這個準備階段

static int value = 3;//類變量 初始化,設為默認值 0,不是 3哦 !!!
int num = 4;//類成員變量,在這個階段不初始化;在 new類,調用對應類的構造函數才進行初始化
final static valFin = 5;//這個比較特殊,在這個階段也不會分配內存!!!

註意: valFin 是被final static修飾的常量在 **編譯 **的時候已分配好瞭,所以在準備階段 此時的值為5,所以在這個階段也不會初始化!

4.解析

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

符號引用就是一組符號來描述目標,可以是任何字面量。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

這個階段瞭解一下即可

5.初始化

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

初始化階段 是類加載過程的最後一個步驟,之前介紹的幾個類加載的動作裡,除瞭在加載階段用戶應用程序可以通過自定義類加載器的方式局部參與外,其餘動作都完全由Java虛擬機來主導控 制。

Java程序對類的使用方式可分為兩種:主動使用被動使用。一般來說隻有當對類的首次主動使用的時候才會導致類的初始化,所以主動使用又叫做類加載過程中“初始化”開始的時機。

類實例初始化方式,主要是以下幾種:

1、創建類的實例,也就是new的方式

2、訪問某個類或接口的靜態變量,或者對該靜態變量賦值

3、調用類的靜態方法

4、反射(如Class.forName("com.test.Person")

5、初始化某個類的子類,則其父類也會被初始化

6、Java虛擬機啟動時被標明為啟動類的類(JavaTest),還有就是Main方法的類會 首先被初始化

這邊就不展開說瞭,大傢記住即可

6.使用

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

7.卸載

當用戶程序代碼執行完畢後,JVM便開始銷毀創建的Class對象,最後負責運行的JVM也退出內存

在如下幾種情況下,Java虛擬機將結束生命周期

執行瞭System.exit()方法

程序正常執行結束

程序在執行過程中遇到瞭異常或錯誤而異常終止

由於操作系統出現錯誤而導致Java虛擬機進程終止

類加載器 與 雙親委派機制

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

那什麼是類加載器?通過一個類的全限定名來獲取描述此類的二進制字節流到JVM中,然後轉換為一個與目標類對應的java.lang.Class對象實例

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

  • 引導類加載器(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
  • 擴展類加載器(Extension ClassLoader):它用來加載/jre/lib/ext目錄以及java.ext.dirs系統變量指定的類路徑下的類。
  • 應用類加載器(AppClassLoader):它主要加載應用程序ClassPath下的類(包含jar包中的類)。它是java應用程序默認的類加載器。其實就是加載我們一般開發使用的類
  • 用戶自定義類加載器:用戶根據自定義需求,自由的定制加載的邏輯,隻需繼承應用類加載器AppClassLoader,負責加載用戶自定義路徑下的class字節碼文件
  • 線程上下文類加載器:除瞭以上列舉的三種類加載器,其實還有一種比較特殊的類型就是線程上下文類加載器。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;
        }
    }

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

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

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

雙親委派模式優勢:

  • 避免類的重復加載, 當父親已經加載瞭該類時,就沒有必要子ClassLoader再加載一次, 這樣保證瞭每個類隻被加載一次。
  • 保護程序安全,防止核心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文件大致流程:

  • 先去緩存中 檢查是否已經加載該類,有就直接返回,避免重復加載;沒有就下一步
  • 遵循 雙親委派機制,來加載.class文件
  • 上面兩步都失敗瞭,調用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內置的一種服務發現機制,將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要,其核心思想就是 讓服務定義與實現分離解耦

SPI機制圖

2.線程上下文類加載器(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可以部署多個應用程序。

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

Tomcat類加載器種類

  • 如果Tomcat本身的依賴和Web應用還需要共享,Common類加載器(CommonClassLoader)來裝載實現共享
  • Catalina類加載器(CatalinaClassLoader) 用來 隔絕Web應用程序與Tomcat本身的類
  • Shared類加載器(SharedClassLoader):如果WebAppClassLoader自身沒有加載到某個類,那就委托SharedClassLoader去加載
  • WebAppClassLoader:為瞭實現隔離性,優先加載 Web 應用自己定義的類,所以沒有遵照雙親委派的約定,每一個應用自己的類加載器WebAppClassLoader(多個應用程序,就有多個WebAppClassLoader)負責優先加載本身的目錄下的class文件加載不到時再交給CommonClassLoader以及上層的ClassLoader進行加載這破壞瞭雙親委派機制。
  • 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類加載器 雙親委派機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: