4.自定義類加載器實現及在tomcat中的應用


了解了類加載器的雙親委派機制, 也知道了雙親委派機制的原理,接下來就是檢驗我們學習是否扎實了,來自定義一個類加載器

一. 回顧類加載器的原理

還是這張圖,類加載器的入口是c++調用java代碼創建了JVM啟動器,其中的一個啟動器是sun.misc.Launcher啟動器。這個啟動器啟動並加載的AppClassLoader和ExtClassLoader。然后調用launcher.getClassLoader()方法獲取loader對象, loader對象本質是一個ClassLoader,然后調用了ClassLoader的loadClass("...")方法加載類。也是在loadClass("...")方法里實現了雙親委派機制。

詳細原理參考文章:https://www.cnblogs.com/ITPower/p/15363400.html

二、自定義類加載器分析

對於類加載器, 我們知道他的重點是loadClass(...)方法, 里面的雙親委派機制也是在loadClass方法里面實現的. loadClass方法里面實際上去加載類的是findClass()方法. 對於我們自定義的類加載器來說需要做到兩點即可

  1. 這個自定義的類加載器繼承自ClassLoader

  2. 這個類加載器要重寫ClassLoader類中的findClass()方法

另外我們還可以參考AppClassLoader和ExtClassLoader來寫。

三、自定義類加載器實現

下面我自己定義了一個類加載器

第一步:自定義類加載器繼承自ClassLoader抽象類,然后定義一個構造方法, 用來接收要加載的類名

第二步:重寫核心方法findClass(String name)

這里有兩步操作,

第一個是: 從類路徑中讀取要加載類的文件內容, 自定義

第二個是: 調用構造類的方法, 調用的系統的defineClass

接下來看看自定義的loadByte是如何實現的

這里的實現就是找到類, 並且將類的內容讀取出來, 轉換成二進制的字節碼, 返回

最后一部分就是如何調用了.

用類加載器加載類, 然后實例化, 使用反射機制調用User1 的方法sout

package com.lxl.jvm;

public class User1 {
    public void sout() {
        System.out.println("進入到User1");
    }
}

這里面System.out.println(clazz.getClassLoader().getClass().getName()); 獲取當前類的類加載器, 猜一猜這里打印的會是誰?

看到了么? 是AppClassLoader, 為什么呢?

原因是我的項目里已經有一個類User1了

我們自定義類加載器的父類是AppClassLoader. 而程序代碼中的User1剛好是被AppClassLoader加載, 因為找到了,所以就不會再去我們指定的文件夾中查找了

這就是類的雙親委派機制的特點.

那么如果我們將項目中的User1類刪除掉, 這是類加載器是誰呢? 當然就是我們自定義的類加載器了.

那么問題來了, 自定義類加載器的父類為什么是AppClassLoader呢?

四. 分析自定義類加載的父類為什么是appClassLoader?

我們來看一下源碼

我們自定義的類加載器, 繼承自ClassLoader類加載器, 那么在調用自定義類加載器的構造方法之前, 應該先加載父類ClassLoader的無參構造函數.

首先會執行ClassLoader的無參的構造方法.

而無參的構造方法會調用自身的構造方法

里面有一個parent, 我們就是要看看這個parent到底是誰呢. 來看看getSystemClassLoader()方法

之前我們已經研究過getClassLoader()這個方法了, 這里面定義的loadClass是誰呢?就是AppClassLoader.

這就是為什么自定義class類加載器的父類是AppClassLoader的原因了。

五、打破雙親委派機制

首先,我們要明白,什么是雙親委派機制?為什么要打破雙親委派機制?什么時候需要打破雙親委派機制?

1. 什么是雙親委派機制?

在前面,我們說了什么是雙親委派機制,子類委托父類加載的這個邏輯,就是雙親委派機制。如果還不知道什么是雙親委派機制,可以查看文章:https://www.cnblogs.com/ITPower/p/15363400.html

2. 如何打破雙親委派機制呢?

我們知道了,雙親委派機制就是類在加載的時候,從自定義類加載器開始查找是否已經加載過這個類,如果沒有加載過則加載類,但是不是由自己立刻加載,而是委托上級加載。到了上級,先查找,找不到在加載,然后也不是自己立刻加載,依次類推。。。。這就是雙親委派機制,要打破雙親委派機制,那么就是不讓他委托上級類加載器加載,由自己來加載。那么如何實現呢?

比如, 我現在有一個自定義類加載器, 加載的是~/com/lxl/jvm/User1.class類, 而在應用程序的target目錄下也有一個com/lxl/jvm/User1.class, 那么, 最終User1.class這個類將被哪個類加載器加載呢? 根據雙親委派機制, 我們知道, 他一定是被應用程序類加載器AppClassLoader加載, 而不是我們自定義的類加載器, 為什么呢? 因為他要向上尋找, 向下委托. 當找到了以后, 便不再向后執行了.

我們要打破雙親委派機制, 就是要讓自定義類加載器來加載我們的User1.class, 而不是應用程序類加載器來加載

雙親委派機制是在ClassLoader類的loadClass(...)方法實現的. 如果我們不想使用系統自帶的雙親委派模式, 只需要重新實現ClassLoader的loadClass(...)方法即可. 下面是ClassLoader中定義的loadClass()方法. 里面實現了雙親委派機制

下面給DefinedClassLoaderTest.java增加一個loadClass方法, 拷貝上面的代碼即可. 刪除掉中間實現雙親委派機制的部分

這里需要注意的是, com.lxl.jvm是自定義的類包, 只有我們自己定義的類才從這里加載. 如果是系統類, 依然使用雙親委派機制來加載.

來看看運行結果:

調用了user1的sout方法
com.lxl.jvm.DefinedClassLoaderTest

現在User1方法確實是由自定義類加載器加載的了

源碼:

package com.lxl.jvm;

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

/**
 * 自定義的類加載器
 */
public class DefinedClassLoaderTest extends ClassLoader{

    private String classPath;

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

    /**
     * 重寫findClass方法
     *
     * 如果不會寫, 可以參考URLClassLoader中是如何加載AppClassLoader和ExtClassLoader的
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadBytes(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    private byte[] loadBytes(String name) throws Exception {
        // 我們需要讀取類的路徑
        String path = name.replace('.', '/').concat(".class");
        //String path = "";
        // 去路徑下查找這個類
        FileInputStream fileInputStream = new FileInputStream(classPath + "/"  + path);
        int len = fileInputStream.available();

        byte[] data = new byte[len];
        fileInputStream.read(data);
        fileInputStream.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) {
                /**
                 * 直接執行findClass()...什么意思呢? 首先會使用自定義類加載器加載類, 不在向上委托, 直接由
                 * 自己執行
                 *
                 * jvm自帶的類還是需要由引導類加載器自動加載
                 */
                if (!name.startsWith("com.lxl.jvm")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    public static void main(String[] args) throws Exception {
        DefinedClassLoaderTest classLoader = new DefinedClassLoaderTest("/Users/luoxiaoli");
        Class<?> clazz = classLoader.loadClass("com.lxl.jvm.User1");
        Object obj = clazz.newInstance();
        Method sout = clazz.getDeclaredMethod("sout", null);
        sout.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }

} 

六. 打破雙親委派機制案例--tomcat部署多應用?

1. tomcat為何要打破雙親委派機制?

通常,我們在服務器安裝的一個tomcat下會部署多個應用。而這多個應用可能使用的類庫的版本是不同的。比如:項目A使用的是spring4,項目B使用的是Spring5。Spring4和Spring5多數類都是一樣的,但是有個別類有所不同,這些不同是類的內容不同,而類名,包名都是一樣的。假如,我們采用jdk向上委托的方式,項目A在部署的時候,應用類加載器加載了他的類。在部署項目B的時候,由於類名相同,這是應用服務器就不會再次加載同包同名的類。這樣就會有問題。所以, tomcat需要打破雙親委派機制。不同的war包下的類自己加載,而不向上委托。基礎類依然向上委托。

2.tomcat是如何打破雙親委派機制的?

實際上, 我們的tomcat可以加載各種各樣類型的war包, 相互之間沒有影響. 因為tomcat打破了雙親委派機制, 下面我們就來看看tomcat是如何打破雙親委派機制的?

如上圖, 上面的橙色部門還是和原來一樣, 采用雙親委派機制. 而黃色部分是tomcat第一部分自定義的類加載器, 這部分主要是加載tomcat包中的類, 這一部分依然采用的是雙親委派機制, 而綠色部分是tomcat第二部分自定義類加載器, 正事這一部分, 打破了類的雙親委派機制. 先面我們就來詳細看看tomcat自定義的類加載器

1. tomcat第一部分自定義類加載器(黃色部分)

這部分類加載器, 在tomcat7及以前是tomcat自定義的三個類加載器, 分別加載不同文件家下的jar包. 而到了tomcat7及以后, tomcat將這三個文件夾合並了, 合並成了一個lib包. 也就是我們現在看到的lib包

我們來看看這三個類加載器的主要功能.

  • commonClassLoader: tomcat最基本的類加載器, 加載路徑中的class可以被tomcat容器本身和各個webapp訪問;
  • catalinaClassLoader: tomcat容器中私有的類加載器, 加載路徑中的class對於webapp不可見的部分。
  • sharedClassLoader: 各個webapps共享的類加載器, 加載路徑中的class對於所有的webapp都可見, 但是對於tomcat容器不可見.

這一部分類加載器, 依然采用的是雙親委派機制, 原因是, 他只有一份. 如果有重復, 那么也是以這一份為准. 這部分主要加載的是tomcat自帶的類。

2.tomcat第二部分自定義類加載器(綠色部分)

綠色部分是java項目在打war包的時候, tomcat自動生成的類加載器, 也就是說 , 每一個項目打成一個war包, tomcat都會自動生成一個類加載器, 專門用來加載這個war包. 而這個類加載器打破了雙親委派機制. 我們可以想象一下, 假如這個webapp類加載器沒有打破雙親委派機制會怎么樣?

之前也說過,如果沒有打破, 他就會委托父類加載器去加載, 一旦加載到了, 子類加載器就沒有機會在加載了. 那么, spring4和spring5的項目想共存, 那是不可能的了.

所以, 這一部分他打破了雙親委派機制

這樣一來, webapp類加載器不需要在讓上級去加載, 他自己就可以加載對應war里的class文件. 當然了, 其他的基礎項目文件, 還是要委托上級加載的.

下面我們來實現一個自定義的tomcat類加載器

3.自定義tomcat的war包類加載器

如何打破雙親委派機制, 我們在上面已經寫過一個demo了.

那么, 現在我有兩個war包, 分處於不同的文件夾, tomcat如何使用各自的類加載器加載自己包下的class類呢?

我們來舉個例子, 比如: 在我的home目錄下有兩個文件夾, tomcat-test和tomcat-test1. 用這兩個文件夾來模擬兩個項目.

在他們的下面都有一個com/lxl/jvm/User1.class

雖然類名和類路徑都是一樣的,但是他們的內容是不同的

這個時候,如果tomcat要同時加載這兩個目錄下的User1.class文件, 我們如何操作呢?

其實,非常簡單, 按照上面的思路, tomcat只需要為每一個文件夾生成一個新的類加載器就可以了.

public static void main(String[] args) throws Exception {
     // 第一個類加載器
        DefinedClassLoaderTest classLoader = new DefinedClassLoaderTest("/Users/app/tomcat-test");
        Class<?> clazz = classLoader.loadClass("com.lxl.jvm.User1");
        Object obj = clazz.newInstance();
        Method sout = clazz.getDeclaredMethod("sout", null);
        sout.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());


     // 第二個類加載器
        DefinedClassLoaderTest classLoader1 = new DefinedClassLoaderTest("/Users/app/tomcat-test1");
        Class<?> clazz1 = classLoader1.loadClass("com.lxl.jvm.User1");
        Object obj1 = clazz1.newInstance();
        Method sout1 = clazz1.getDeclaredMethod("sout", null);
        sout1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader().getClass().getName());
    }

他們都是只加載自己目錄下的文件. 我們來看看執行結果:

調用了user1的sout方法
com.lxl.jvm.DefinedClassLoaderTest

調用了另外一個項目user1的sout方法, 他們是不同的
com.lxl.jvm.DefinedClassLoaderTest

雖然上面的代碼很簡單,但這就是tomcat加載不同war包的原理。不同的是,tomcat實現邏輯會更復雜,他的類加載器都是動態生成的。精髓都是一樣的。

4. 思考: tomcat自定義的類加載器中, 有一個jsp類加載器,jsp是可以實現熱部署的, 那么他是如何實現的呢?

jsp其實是一個servlet容器, 由tomcat加載. tomcat會為每一個jsp生成一個類加載器. 這樣每個類加載器都加載自己的jsp, 不會加載別人的. 當jsp文件內容修改時, tomcat會有一個監聽程序來監聽jsp的改動. 比如文件夾的修改時間, 一旦時間變了, 就重新加載文件夾中的內容.

具體tomcat是怎么實現的呢? tomcat自定義了一個thread, 用來監聽不同文件夾中文件的內容是否修改, 如何監聽呢? 就看文件夾的update time有沒有變化, 如果有變化了, 那么就會重新加載.

jsp熱部署也不是立刻就會看到效果,其他他也是有延遲的,這個延遲就是重新加載的過程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM