熱加載是指能夠在不重啓服務的狀況下讓更改的代碼生效,熱加載能夠顯著的提高開發以及調試的效率,它是基於 Java 的類加載器實現的,可是因爲熱加載的不安全性,通常不會用於正式的生產環境。html
<!-- more -->java
首先,不論是熱加載仍是熱部署,均可以在不重啓服務的狀況下編譯/部署項目,都是基於 Java 的類加載器實現的。git
那麼二者到底有什麼區別呢?github
在部署方式上:面試
在實現原理上:shell
在使用場景上:數組
可能你已經發現了,圖中一共是7個階段,而不是5個。是由於圖是類的完整生命週期,若是要說只是類加載階段的話,圖裏最後的使用(Using)和卸載(Unloading)並不算在內。緩存
簡單描述一下類加載的五個階段:安全
加載階段:找到類的靜態存儲結構,加載到虛擬機,定義數據結構。用戶能夠自定義類加載器。服務器
驗證階段:確保字節碼是安全的,確保不會對虛擬機的安全形成危害。
準備階段:肯定內存佈局,肯定內存遍歷,賦初始值(注意:是初始值,也有特殊狀況)。
解析階段: 將符號變成直接引用。
初始化階段:調用程序自定義的代碼。規定有且僅有5種狀況必須進行初始化。
要說明的是,類加載的 5 個階段中,只有加載階段是用戶能夠自定義處理的,而驗證階段、準備階段、解析階段、初始化階段都是用 JVM 來處理的。
咱們怎麼才能手動寫一個類的熱加載呢?根據上面的分析,Java 程序在運行的時候,首先會把 class 類文件加載到 JVM 中,而類的加載過程又有五個階段,五個階段中只有加載階段用戶能夠進行自定義處理,因此咱們若是能在程序代碼更改且從新編譯後,讓運行的進程能夠實時獲取到新編譯後的 class 文件,而後從新進行加載的話,那麼理論上就能夠實現一個簡單的 Java 熱加載。
因此咱們能夠得出實現思路:
設計 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; } }
咱們假設某個接口(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; } }
在實現思路里,咱們知道輪訓檢查 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; } }
直接寫一個線程不斷的檢測要熱加載的類是否是已經更改須要從新加載,而後運行測試便可。
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