Java代碼自定義類加載器實作熱部署
一.思路
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 測試結果來張圖