原來熱加載如此簡單,手動寫一個 Java 熱加載吧

1. 什麼是熱加載

熱加載是指能夠在不重啓服務的狀況下讓更改的代碼生效,熱加載能夠顯著的提高開發以及調試的效率,它是基於 Java 的類加載器實現的,可是因爲熱加載的不安全性,通常不會用於正式的生產環境。html

<!-- more -->java

2. 熱加載與熱部署的區別

首先,不論是熱加載仍是熱部署,均可以在不重啓服務的狀況下編譯/部署項目,都是基於 Java 的類加載器實現的。git

那麼二者到底有什麼區別呢?github

在部署方式上:面試

  • 熱部署是在服務器運行時從新部署項目。
  • 熱加載是在運行時從新加載 class

在實現原理上:shell

  • 熱部署是直接從新加載整個應用,耗時相對較高。
  • 熱加載是在運行時從新加載 class,後臺會啓動一個線程不斷檢測你的類是否改變。

在使用場景上:數組

  • 熱部署更多的是在生產環境使用。
  • 熱加載則更多的是在開發環境上使用。線上因爲安全性問題不會使用,難以監控。

3. 類加載五個階段

類的生命週期

可能你已經發現了,圖中一共是7個階段,而不是5個。是由於圖是類的完整生命週期,若是要說只是類加載階段的話,圖裏最後的使用(Using)和卸載(Unloading)並不算在內。緩存

簡單描述一下類加載的五個階段:安全

  1. 加載階段:找到類的靜態存儲結構,加載到虛擬機,定義數據結構。用戶能夠自定義類加載器。服務器

  2. 驗證階段:確保字節碼是安全的,確保不會對虛擬機的安全形成危害。

  3. 準備階段:肯定內存佈局,肯定內存遍歷,賦初始值(注意:是初始值,也有特殊狀況)。

  4. 解析階段: 將符號變成直接引用。

  5. 初始化階段:調用程序自定義的代碼。規定有且僅有5種狀況必須進行初始化。

    1. new(實例化對象)、getstatic(獲取類變量的值,被final修飾的除外,他的值在編譯器時放到了常量池)、putstatic(給類變量賦值)、invokestatic(調用靜態方法) 時會初始化
    2. 調用子類的時候,發現父類尚未初始化,則父類須要當即初始化。
    3. 虛擬機啓動,用戶要執行的主類,主類須要當即初始化,如 main 方法。
    4. 使用 java.lang.reflect包的方法對類進行反射調用方法 是會初始化。
    5. 當使用JDK 1.7的動態語言支持時, 若是一個java.lang.invoke.MethodHandle實例最後 的解析結果REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄, 而且這個方法句柄 所對應的類沒有進行過初始化, 則須要先觸發其初始化。

要說明的是,類加載的 5 個階段中,只有加載階段是用戶能夠自定義處理的,而驗證階段、準備階段、解析階段、初始化階段都是用 JVM 來處理的。

4. 實現類的熱加載

4.1 實現思路

咱們怎麼才能手動寫一個類的熱加載呢?根據上面的分析,Java 程序在運行的時候,首先會把 class 類文件加載到 JVM 中,而類的加載過程又有五個階段,五個階段中只有加載階段用戶能夠進行自定義處理,因此咱們若是能在程序代碼更改且從新編譯後,讓運行的進程能夠實時獲取到新編譯後的 class 文件,而後從新進行加載的話,那麼理論上就能夠實現一個簡單的 Java 熱加載

因此咱們能夠得出實現思路:

  1. 實現本身的類加載器。
  2. 從本身的類加載器中加載要熱加載的類。
  3. 不斷輪訓要熱加載的類 class 文件是否有更新。
  4. 若是有更新,從新加載。

4.2 自定義類加載器

設計 Java 虛擬機的團隊把類的加載階段放到的 JVM 的外部實現( 經過一個類的全限定名來獲取描述此類的二進制字節流 )。這樣就可讓程序本身決定若是獲取到類信息。而實現這個加載動做的代碼模塊,咱們就稱之爲 「類加載器」。

在 Java 中,類加載器也就是 java.lang.ClassLoader. 因此若是咱們想要本身實現一個類加載器,就須要繼承 ClassLoader 而後重寫裏面 findClass的方法,同時由於類加載器是 雙親委派模型實現(也就說。除了一個最頂層的類加載器以外,每一個類加載器都要有父加載器,而加載時,會先詢問父加載器可否加載,若是父加載器不能加載,則會本身嘗試加載)因此咱們還須要指定父加載器。

最後根據傳入的類路徑,加載類的代碼看下面。

package net.codingme.box.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

/**
 * <p>
 * 自定義 Java類加載器來實現Java 類的熱加載
 *
 * @Author niujinpeng
 * @Date 2019/10/24 23:22
 */
public class MyClasslLoader extends ClassLoader {

    /** 要加載的 Java 類的 classpath 路徑 */
    private String classpath;

    public MyClasslLoader(String classpath) {
        // 指定父加載器
        super(ClassLoader.getSystemClassLoader());
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = this.loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }

    /**
     * 加載 class 文件中的內容
     *
     * @param name
     * @return
     */
    private byte[] loadClassData(String name) {
        try {
            // 傳進來是帶包名的
            name = name.replace(".", "//");
            FileInputStream inputStream = new FileInputStream(new File(classpath + name + ".class"));
            // 定義字節數組輸出流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while ((b = inputStream.read()) != -1) {
                baos.write(b);
            }
            inputStream.close();
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

4.3 定義要類型熱加載的類

咱們假設某個接口(BaseManager.java)下的某個方法(logic)要進行熱加載處理。

首先定義接口信息。

package net.codingme.box.classloader;

/**
 * <p>
 * 實現這個接口的子類,須要動態更新。也就是熱加載
 *
 * @Author niujinpeng
 * @Date 2019/10/24 23:29
 */
public interface BaseManager {

    public void logic();
}

寫一個這個接口的實現類。

package net.codingme.box.classloader;

import java.time.LocalTime;

/**
 * <p>
 * BaseManager 這個接口的子類要實現類的熱加載功能。
 *
 * @Author niujinpeng
 * @Date 2019/10/24 23:30
 */
public class MyManager implements BaseManager {

    @Override
    public void logic() {
        System.out.println(LocalTime.now() + ": Java類的熱加載");
    }
}

後面咱們要作的就是讓這個類能夠經過咱們的 MyClassLoader 進行自定義加載。類的熱加載應當只有在類的信息被更改而後從新編譯以後進行從新加載。因此爲了避免意義的重複加載,咱們須要判斷 class 是否進行了更新,因此咱們須要記錄 class 類的修改時間,以及對應的類信息。

因此編譯一個類用來記錄某個類對應的某個類加載器以及上次加載的 class 的修改時間。

package net.codingme.box.classloader;

/**
 * <p>
 * 封裝加載類的信息
 *
 * @Author niujinpeng
 * @Date 2019/10/24 23:32
 */
public class LoadInfo {

    /** 自定義的類加載器 */
    private MyClasslLoader myClasslLoader;

    /** 記錄要加載的類的時間戳-->加載的時間 */
    private long loadTime;

    /** 須要被熱加載的類 */
    private BaseManager manager;

    public LoadInfo(MyClasslLoader myClasslLoader, long loadTime) {
        this.myClasslLoader = myClasslLoader;
        this.loadTime = loadTime;
    }

    public MyClasslLoader getMyClasslLoader() {
        return myClasslLoader;
    }

    public void setMyClasslLoader(MyClasslLoader myClasslLoader) {
        this.myClasslLoader = myClasslLoader;
    }

    public long getLoadTime() {
        return loadTime;
    }

    public void setLoadTime(long loadTime) {
        this.loadTime = loadTime;
    }

    public BaseManager getManager() {
        return manager;
    }

    public void setManager(BaseManager manager) {
        this.manager = manager;
    }
}

4.4 熱加載獲取類信息

在實現思路里,咱們知道輪訓檢查 class 文件是否是被更新過,因此每次調用要熱加載的類時,咱們都要進行檢查類是否被更新而後決定要不要從新加載。爲了方便這步的獲取操做,可使用一個簡單的工廠模式進行封裝。

要注意是加載 class 文件須要指定完整的路徑,因此類中定義了 CLASS_PATH 常量。

package net.codingme.box.classloader;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 加載 manager 的工廠
 *
 * @Author niujinpeng
 * @Date 2019/10/24 23:38
 */
public class ManagerFactory {

    /** 記錄熱加載類的加載信息 */
    private static final Map<String, LoadInfo> loadTimeMap = new HashMap<>();

    /** 要加載的類的 classpath */
    public static final String CLASS_PATH = "D:\\IdeaProjectMy\\lab-notes\\target\\classes\\";

    /** 實現熱加載的類的全名稱(包名+類名 ) */
    public static final String MY_MANAGER = "net.codingme.box.classloader.MyManager";

    public static BaseManager getManager(String className) {
        File loadFile = new File(CLASS_PATH + className.replaceAll("\\.", "/") + ".class");
        // 獲取最後一次修改時間
        long lastModified = loadFile.lastModified();
        System.out.println("當前的類時間:" + lastModified);
        // loadTimeMap 不包含 ClassName 爲 key 的信息,證實這個類沒有被加載,要加載到 JVM
        if (loadTimeMap.get(className) == null) {
            load(className, lastModified);
        } // 加載類的時間戳變化了,咱們一樣要從新加載這個類到 JVM。
        else if (loadTimeMap.get(className).getLoadTime() != lastModified) {
            load(className, lastModified);
        }
        return loadTimeMap.get(className).getManager();
    }

    /**
     * 加載 class ,緩存到 loadTimeMap
     * 
     * @param className
     * @param lastModified
     */
    private static void load(String className, long lastModified) {
        MyClasslLoader myClasslLoader = new MyClasslLoader(className);
        Class loadClass = null;
        // 加載
        try {
            loadClass = myClasslLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        BaseManager manager = newInstance(loadClass);
        LoadInfo loadInfo = new LoadInfo(myClasslLoader, lastModified);
        loadInfo.setManager(manager);
        loadTimeMap.put(className, loadInfo);
    }

    /**
     * 以反射的方式建立 BaseManager 的子類對象
     * 
     * @param loadClass
     * @return
     */
    private static BaseManager newInstance(Class loadClass) {
        try {
            return (BaseManager)loadClass.getConstructor(new Class[] {}).newInstance(new Object[] {});
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return null;
    }
}

4.5 熱加載測試

直接寫一個線程不斷的檢測要熱加載的類是否是已經更改須要從新加載,而後運行測試便可。

package net.codingme.box.classloader;

/**
 * <p>
 *
 * 後臺啓動一條線程,不斷檢測是否要刷新從新加載,實現了熱加載的類
 * 
 * @Author niujinpeng
 * @Date 2019/10/24 23:53
 */
public class MsgHandle implements Runnable {
    @Override
    public void run() {
        while (true) {
            BaseManager manager = ManagerFactory.getManager(ManagerFactory.MY_MANAGER);
            manager.logic();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

主線程:

package net.codingme.box.classloader;

public class ClassLoadTest {
    public static void main(String[] args) {
        new Thread(new MsgHandle()).start();
    }
}

代碼已經所有準備好了,最後一步,能夠啓動測試了。若是你是用的是 Eclipse ,直接啓動就好了;若是是 IDEA ,那麼你須要 DEBUG 模式啓動(IDEA 對熱加載有必定的限制)。

啓動後看到控制檯不斷的輸出:

00:08:13.018: Java類的熱加載
00:08:15.018: Java類的熱加載

這時候咱們隨便更改下 MyManager 類的 logic 方法的輸出內容而後保存。

@Override
public void logic() {
     System.out.println(LocalTime.now() + ": Java類的熱加載 Oh~~~~");
}

能夠看到控制檯的輸出已經自動更改了(IDEA 在更改後須要按 CTRL + F9)。

代碼已經放到Github: https://github.com/niumoo/lab-notes/

<完>

我的網站:https://www.codingme.net
若是你喜歡這篇文章,能夠關注公衆號,文章第一時間直達 。
關注公衆號回覆資源能夠沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。

原文出處:https://www.cnblogs.com/niumoo/p/11756703.html

相關文章
相關標籤/搜索