如何本身手寫一個熱加載

如何本身手寫一個熱加載

熱加載:在不中止程序運行的狀況下,對類(對象)的動態替換html

Java ClassLoader 簡述

Java中的類從被加載到內存中到卸載出內存爲止,一共經歷了七個階段:加載、驗證、準備、解析、初始化、使用、卸載。java

接下來咱們重點講解加載和初始化這兩步git

加載

在加載的階段,虛擬機須要完成如下三件事:github

  • 經過一個類的全限定名來獲取定義此類的二進制字節流
  • 將這個字節流所表明的的靜態存儲結構轉化爲方法區的運行時數據結構
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

這三步都是經過類加載器來實現的。而官方定義的Java類加載器有BootstrapClassLoaderExtClassLoaderAppClassLoader。這三個類加載器分別負責加載不一樣路徑的類的加載。並造成一個父子結構。數組

類加載器名稱 負責加載目錄
BootstrapClassLoader 處於類加載器層次結構的最高層,負責 sun.boot.class.path 路徑下類的加載,默認爲 jre/lib 目錄下的核心 API 或 -Xbootclasspath 選項指定的 jar 包
ExtClassLoader 加載路徑爲 java.ext.dirs,默認爲 jre/lib/ext 目錄或者 -Djava.ext.dirs 指定目錄下的 jar 包加載
AppClassLoader 加載路徑爲 java.class.path,默認爲環境變量 CLASSPATH 中設定的值。也能夠經過 -classpath 選型進行指定

默認狀況下,例如咱們使用關鍵字new或者Class.forName都是經過AppClassLoader 類加載器來加載的數據結構

正由於是此父子結構,因此默認狀況下若是要加載一個類,會優先將此類交給其父類進行加載(直到頂層的BootstrapClassLoader 也沒有),若是父類都沒有,那麼纔會將此類交給子類加載。這就是類加載器的雙親委派規則。ide

初始化

當咱們要使用一個類的執行方法或者屬性時,類必須是加載到內存中而且完成初始化的。那麼類是何時被初始化的呢?有如下幾種狀況優化

  • 使用new關鍵字實例化對象的時候、讀取或者設置一個類的靜態字段、以及調用一個類的靜態方法。
  • 使用java.lang.reflect包的方法對類進行反射調用時,若是類沒有進行初始化,那麼先進行初始化。
  • 初始化一個類的時候,若是發現其父類沒有進行初始化,則先觸發父類的初始化。
  • 當虛擬機啓動時,用戶須要制定一個執行的主類(包含main()方法的那個類)虛擬機會先初始化這個主類。

如何實現熱加載?

在上面咱們知道了在默認狀況下,類加載器是遵循雙親委派規則的。因此咱們要實現熱加載,那麼咱們須要加載的那些類就不能交給系統加載器來完成。因此咱們要自定義類加載器來寫咱們本身的規則。this

實現本身的類加載器

要想實現本身的類加載器,只須要繼承ClassLoader類便可。而咱們要打破雙親委派規則,那麼咱們就必需要重寫loadClass方法,由於默認狀況下loadClass方法是遵循雙親委派的規則的。spa

public class CustomClassLoader extends ClassLoader{

    private static final String CLASS_FILE_SUFFIX = ".class";

    //AppClassLoader的父類加載器
    private ClassLoader extClassLoader;

    public CustomClassLoader(){
        ClassLoader j = String.class.getClassLoader();
        if (j == null) {
            j = getSystemClassLoader();
            while (j.getParent() != null) {
                j = j.getParent();
            }
        }
        this.extClassLoader = j ;
    }

    protected Class<?> loadClass(String name, boolean resolve){

        Class cls = null;
        cls = findLoadedClass(name);
        if (cls != null){
            return cls;
        }
        //獲取ExtClassLoader
        ClassLoader extClassLoader = getExtClassLoader() ;
        //確保自定義的類不會覆蓋Java的核心類
        try {
            cls = extClassLoader.loadClass(name);
            if (cls != null){
                return cls;
            }
        }catch (ClassNotFoundException e ){

        }
        cls = findClass(name);
        return cls;
    }

    @Override
    public Class<?> findClass(String name) {
        byte[] bt = loadClassData(name);
        return defineClass(name, bt, 0, bt.length);
    }

    private byte[] loadClassData(String className) {
        // 讀取Class文件呢
        InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/")+CLASS_FILE_SUFFIX);
        ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
        // 寫入byteStream
        int len =0;
        try {
            while((len=is.read())!=-1){
                byteSt.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 轉換爲數組
        return byteSt.toByteArray();
    }

    public ClassLoader getExtClassLoader(){
        return extClassLoader;
    }
}

爲何要先獲取ExtClassLoader類加載器呢?其實這裏是借鑑了Tomcat裏面的設計,是爲了不咱們自定義的類加載器覆蓋了一些核心類。例如java.lang.Object

爲何是獲取ExtClassLoader 類加載器而不是獲取AppClassLoader 呢?這是由於若是咱們獲取了AppClassLoader 進行加載,那麼不仍是雙親委派的規則了嗎?

監控class文件

這裏咱們使用ScheduledThreadPoolExecutor 來進行週期性的監控文件是否修改。在程序啓動的時候記錄文件的最後修改時間。隨後週期性的查看文件的最後修改時間是否改動。若是改動了那麼就從新生成類加載器進行替換。這樣新的文件就被加載進內存中了。

首先咱們創建一個須要監控的文件

public class Test {

    public void test(){
        System.out.println("Hello World! Version one");
    }
}

咱們經過在程序運行時修改版本號,來動態的輸出版本號。接下來咱們創建週期性執行的任務類。

public class WatchDog implements Runnable{

    private Map<String,FileDefine> fileDefineMap;

    public WatchDog(Map<String,FileDefine> fileDefineMap){
        this.fileDefineMap = fileDefineMap;
    }

    @Override
    public void run() {
        File file = new File(FileDefine.WATCH_PACKAGE);
        File[] files = file.listFiles();
        for (File watchFile : files){
            long newTime = watchFile.lastModified();
            FileDefine fileDefine = fileDefineMap.get(watchFile.getName());
            long oldTime = fileDefine.getLastDefine();
            //若是文件被修改了,那麼從新生成累加載器加載新文件
            if (newTime!=oldTime){
                fileDefine.setLastDefine(newTime);
                loadMyClass();
            }
        }
    }

    public void loadMyClass(){
        try {
            CustomClassLoader customClassLoader = new CustomClassLoader();
            Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
            Object test = cls.newInstance();
            Method method = cls.getMethod("test");
            method.invoke(test);
        }catch (Exception e){
            System.out.println(e);
        }
    }
}

能夠看到在上面的gif演示圖中咱們簡單的實現了熱加載的功能。

優化

在上面的方法調用中咱們是使用了getMethod()方法來調用的。此時或許會有疑問,爲何不直接將newInstance()強轉爲Test類呢?

若是咱們使用了強轉的話,代碼會變成這樣Test test = (Test) cls.newInstance()。可是在運行的時候會拋ClassCastException 異常。這是爲何呢?由於在Java中肯定兩個類是否相等,除了看他們兩個類文件是否相同之外還會看他們的類加載器是否相同。因此即便是同一個類文件,若是是兩個不一樣的類加載器來加載的,那麼它們的類型就是不一樣的。

WatchDog類是由咱們new出來的。因此默認是AppClassLoader 來加載的。因此test 變量的聲明類型是WatchDog 方法中的一個屬性,因此也是由AppClassLoader 來加載的。所以兩個類不相同。

該如何解決呢?問題就出在了=號雙方的類不同,那麼咱們給它搞成同樣不就好了嗎?怎麼搞?答案就是接口。默認狀況下,若是咱們實現了一個接口,那麼此接口通常都是以子類的加載器爲主的。意思就是若是沒有特殊要求的話,例如A implements B 若是A的加載器是自定義的。那麼B接口的加載器也是和子類是同樣的。

因此咱們要將接口的類加載器搞成是AppClassLoader 來加載。因此自定義加載器中加入這一句

if ("com.example.watchfile.ITest".equals(name)){
    try {
        cls = getSystemClassLoader().loadClass(name);
    } catch (ClassNotFoundException e) {

    }
    return cls;
}

創建接口

public interface ITest {

    void test();
}

這樣咱們就能愉快的調用了。直接調用其方法。不會拋異常,由於=號雙方的類是同樣的。

CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> cls = customClassLoader.loadClass("com.example.watchfile.Test",false);
ITest test = (ITest) cls.newInstance();
test.test();

源代碼地址Github

參考文章

相關文章
相關標籤/搜索