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";
    }
}

 

测试结果来张图