一.思路
0. 監聽java文件最后修改時間,如果發生變化,則表示文件已經修改,進行重新編譯
1. 編譯java文件為 class文件
2. 通過手寫類加載器,加載 class文件 ,創建對象
3. 反射創建對象 / 進行調用,(如果是web項目可以將創建的對象添加到spring容器中)
4. 調用測試
二.知識點
1. 自定義類加載器 繼承 URLClassLoader 或 ClassLoader 都可以,繼承 URLClassLoader 重寫findClass(String name)方法即可實現加載class文件;
2. findClass方法核心語句 :return super.defineClass(String name, byte[] b, int off, int len)方法,b是class文件讀取后得到的byte[]形式;
3. cmd窗口 使用javac即可將java文件編譯成 class文件,在代碼里使用 JavaCompiler 類,調用run方法即可編譯指定java文件為class文件;
4. JavaCompiler不支持直接new,通過類ToolProvider.getSystemJavaCompiler()方法獲取;
5. 通過類加載器獲取的 class文件有時不方便調用,所以可以采用反射調用;
6. 對於一個java文件,可以通過File類的 lastModified獲取最后修改時間,循環比較lastModified即可判斷文件是否被修改;
7. class文件可以生成在任意目錄,通過路徑讀取即可;
8. 選擇合適的類加載器或自定義類加載器,對於電腦上任意位置的class文件完全都可以通過反射調用;
9. @SneakyThrows可以理解成 try-catch,使用需要導入lombok
10. 本demo是通過查閱資料和不斷測試實現,如果有不足請指出;
三.實現
1. Demo概述
目標: 實現對HotTestService類的熱部署,通過測試(main)監控java文件,如果java文件變動調用自定義類加載器MyClassLoader得到HotTestService的class對象,反射調用
2. 核心測試方法

package com.ahd.springtest.utils; import lombok.SneakyThrows; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.TimeUnit; public class AhdgTest { //sourcePath 是java文件存放,編輯的路徑 private static String sourcePath = "D:\\workspace_ms\\20210319\\springtest\\src\\main\\java"; //targetPath 是class文件存放路徑 // private static String targetPath = "D:\\workspace_ms\\20210319\\springtest\\target\\classes"; private static String targetPath = "D:\\workspace_ms\\20210319\\springtest\\src\\main\\java"; private static String errPath = "D:\\文件清單\\hotlog.txt";//編譯日志打印目錄 private static String basePath = "\\com\\ahd\\springtest\\service\\HotTestService"; //包名 + 類名,路徑形式 public static void main(String[] args) throws InterruptedException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, MalformedURLException, ClassNotFoundException { //測試熱部署 testHot(); } /*** * 目標 : main方法循環調用,實時監測 com.ahd.springtest.service.HotTestService 是否發生改變,如果發生改變,重新加載並調用 * * 0. 監聽java文件最后修改時間,如果發生變化,則表示文件已經修改,進行重新編譯 * * 1. 編譯java文件為 class文件 * * 2. 通過手寫類加載器加載 class文件 ,創建對象 * * 3. 將新創建的對象 放入spring容器中 * * 4. 調用測試 * */ public static void testHot() throws MalformedURLException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InterruptedException { // 0. 監聽java文件最后修改時間,(如果發生變化,則表示文件已經修改,需要進行重新編譯) File file = new File(sourcePath + basePath + ".java"); Thread thread = new Thread(new Runnable() { @SneakyThrows //簡化 try catch寫法 @Override public void run() { Long lastModifiedTime = file.lastModified(); while (true) { long timeEnd = file.lastModified(); if (timeEnd != lastModifiedTime) { lastModifiedTime = timeEnd; // 1. 編譯java文件為 class文件 try (InputStream is = new FileInputStream(file.getAbsolutePath()); OutputStream os = new FileOutputStream(targetPath + basePath + ".class"); OutputStream err = new FileOutputStream(errPath)) { JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); javaCompiler.run(is, os, err, sourcePath + basePath + ".java");//前三參數傳入null 默認是 System.in,System.out.System.err } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } // 2. 通過手寫類加載器加載 class文件 ,創建對象 MyClassLoader instance = MyClassLoader.getInstance(new File(targetPath).toURI().toURL()); Class<?> aClass = instance.findClass("com.ahd.springtest.service.HotTestService"); Object o = aClass.newInstance(); // 3. 將新創建的對象 反射調用 Method test = aClass.getMethod("test"); Object invoke = test.invoke(o); System.out.println(invoke); } try { Thread.sleep(20);//檢測頻率:100ms } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread.setDaemon(true);//設置成守護線程 thread.start(); //讓主main一直運行,可以查看結果 while(true){ Thread.sleep(1000); } } }
3. 自定義類加載器MyClassLoader

package com.ahd.springtest.utils; import com.sun.xml.internal.ws.util.ByteArrayBuffer; import lombok.SneakyThrows; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class MyClassLoader extends URLClassLoader { private static MyClassLoader myClassLoader;//可以直接通過getInstance方法獲取 private URL[] urls; private URL url; public MyClassLoader(URL url) { super(new URL[]{url}); this.url = url; } public MyClassLoader(URL[] urls) { super(urls); this.urls = urls; } /*** * 1. name 是 類的 全限命名,通過全限名命 + 路徑 獲取 絕對路徑 * * 2. io獲取字節碼 * * 3. 調用父類方法創建並返回class對象 * @param name * @return * @throws ClassNotFoundException */ @SneakyThrows //簡化的 try catch寫法 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //1. name 是 類的 全限命名,通過全限名命 + 路徑 獲取 絕對路徑 String path = url.getPath(); // System.out.println("yangdc log: " + path); String classPath = path + name.replaceAll("\\.","/").concat(".class"); //2. io獲取字節碼 InputStream is = null; URL url = null; int b = 0; ByteArrayBuffer bab = new ByteArrayBuffer(); try { url = new URL("file:" + classPath); // url = new URL("jar:" + classPath); is = url.openStream(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } while((b = is.read())!=-1){ bab.write(b); } is.close(); //3. 調用父類方法創建並返回class對象 return super.defineClass(name,bab.toByteArray(),0,bab.size()); } public static MyClassLoader getInstance(URL url){ if (myClassLoader == null){ return new MyClassLoader(url); } return myClassLoader; } public static MyClassLoader getInstance(URL[] url){ if (myClassLoader == null){ return new MyClassLoader(url); } return myClassLoader; } public URL[] getUrls() { return urls; } public void setUrls(URL[] urls) { this.urls = urls; } public URL getUrl() { return url; } public void setUrl(URL url) { this.url = url; } }
4. 被測試的類HotTestService
package com.ahd.springtest.service; import org.springframework.stereotype.Service; @Service public class HotTestService { public HotTestService() { } public String test() { return "第39696633次測試hot"; } }
l 測試結果來張圖