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 测试结果来张图