Java基於自定義類載入器實現熱部署過程解析
熱部署:
熱部署就是在不重啟應用的情況下,當類的定義即位元組碼檔案修改後,能夠替換該Class建立的物件。一般情況下,類的載入都是由系統自帶的類載入器完成,且對於同一個全限定名的java類,只能被載入一次,而且無法被解除安裝。可以使用自定義的 ClassLoader 替換系統的載入器,建立一個新的 ClassLoader,再用它載入 Class,得到的 Class 物件就是新的(因為不是同一個類載入器),再用該 Class 物件建立一個例項,從而實現動態更新。如:修改 JSP 檔案即生效,就是利用自定義的 ClassLoader 實現的。
還需要建立一個守護執行緒,不斷地檢查class檔案是否被修改過,通過判斷檔案的上次修改時間實現。
演示:
原來的程式:
修改後重新編譯:
程式碼:
package Dynamic; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.TimeUnit; public class ClassLoadStudy { public static void main(String[] args) throws Exception { HotDeploy hot = new HotDeploy("Dynamic.Task"); hot.monitor(); while (true) { TimeUnit.SECONDS.sleep(2); hot.getTask().run(); } } } // 熱部署 class HotDeploy { private static volatile Runnable instance; private final String FILE_NAME; private final String CLASS_NAME; public HotDeploy(String name) { CLASS_NAME = name; // 類的完全限定名 name = name.replaceAll("\\.","/") + ".class"; FILE_NAME = (getClass().getResource("/") + name).substring(6); // 判斷class檔案修改時間使用,substring(6)去掉開頭的file:/ } // 獲取一個任務 public Runnable getTask() { if (instance == null) { // 雙重檢查鎖,單例,執行緒安全 synchronized (HotDeploy.class) { if (instance == null) { try { instance = createTask(); } catch (Exception e) { e.printStackTrace(); } } } } return instance; } // 建立一個任務,重新載入 class 檔案 private Runnable createTask() { try { Class clazz = MyClassLoader.getLoader().loadClass(CLASS_NAME); if (clazz != null) return (Runnable)clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } return null; } // 監視器,監視class檔案是否被修改過,如果是的話,則重新載入 public void monitor() throws IOException { Thread t = new Thread(()->{ try { long lastModified = Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis(); while(true) { Thread.sleep(500); long now = Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis(); if(now != lastModified) { // 如果class檔案被修改過了 lastModified = now; instance = createTask(); // 重新載入 } } } catch (InterruptedException | IOException e) { e.printStackTrace(); } }); t.setDaemon(true); // 守護執行緒 t.start(); } } // 自定義的類載入器 class MyClassLoader extends ClassLoader { @Override public Class<?> findClass(String name) throws ClassNotFoundException { try { String fileName = "/" + name.replaceAll("\\.","/") + ".class"; InputStream is = getClass().getResourceAsStream(fileName); byte[] b = is.readAllBytes(); return defineClass(name,b,b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } public static MyClassLoader getLoader() { return new MyClassLoader(); } }
遇到的坑:
剛開始自定義類載入器時,重寫的是 loadClass(String name) 方法,但不斷地報錯,後來明白了,因為 Task 類實現了 Java.lang.Runnable 介面,且重寫 loadClass 方法破壞了雙親委派機制,導致了自定義的類載入器去載入 java.lang.Runnable,但被Java安全機制禁止了所以會報錯。defineClass呼叫preDefineClass,preDefineClass 會檢查包名,如果以java開頭,就會丟擲異常,因為讓使用者自定義的類載入器來載入Java自帶的類庫會引起混亂。
於是又重寫findClass 方法,但還是不行,findClass方法總是得不到執行,因為編譯好的類是在 classpath 下的,而自定義的 ClassLoader 的父載入器是 AppClassLoader,由於雙親委派機制,類就會被 Application ClassLoader來載入了。因此自定義的 findClass 方法就不會被執行。解決方法是,向構造器 ClassLoader(ClassLoader parent) 傳入null,或傳入 getSystemClassLoader().getParent()。
還有就是路徑問題:
- path不以 / 開頭時,預設是從此類所在的包下取資源;path 以 / 開頭時,則是從ClassPath根下獲取;
- URL getClass.getResource(String path)
- InputStream getClass().getResourceAsStream(String path)
- getResource("") 返回當前類所在的包的路徑
- getResource("/") 返回當前的 classpath 根據路徑
- path 不能以 / 開始,path 是從 classpath 根開始算的, 因為classloader 不是使用者自定義的類,所以沒有相對路徑的配置檔案可以獲取,所以預設都是從哪個classpath 路徑下讀取,自然就沒有必要以 / 開頭了 。
- URL Class.getClassLoader().getResource(String path)
- InputStream Class.getClassLoader().getResourceAsStream(String path)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。