自定义classloader实现JAVA热替换

来源:互联网 发布:周防尊cos淘宝 编辑:程序博客网 时间:2024/06/04 17:55

这几天学习了下JVM的原理,在看一个视频教程,上面上一个这样的题目:

1. 实现热替换。
   运行一个程序HelloMain,他会循环调用另外一个类Worker.doit()方法。此时,对Worker.doit()方法做更新。要求 更新后,HelloMain可以发现新的版本。


   可以选择替换class文件 ,也可以选择替换jar包。

对于这个题目,让我想起了之前在公司的项目中,有时修复了一个小的BUG,修改的JAVA文件,但为了不重启服务器,节约时间,就之间拿着本地开发环境编译好的CLASS文件,把它放在远程服务器上的tomcat的WEBAPPS的相关项目目录下就可以了。当时是见我们的主管这样做的,第一次看见时觉得很神奇,那时的我还以为所有的JAVA容器都是这样的,只要把calss替换掉,它就会自动对新的类进行替换。通过这几天的学习才知道,原来TOMCAT能达到那样的效果是因为tomcat实现了热替换功能,并且默认启动了热替换功能。

详见

class卸载、热替换和Tomcat的热部署的分析

通过视频的提示以及在网上也看了相关的资料后,决定还是自己动手写一下,强化化代码基础。

由于要把类进行替换,所以必须要定义一个classloader。在上几周,想起了之前参加程序设计竞赛(ACM,天梯赛,蓝桥杯等)时的那些平台,把代码写好后,复制上去,点提交,那个平台就会把程序的运行结果返回给我们。且先不考虑C/C++那些是怎么实现的,只单单考虑JAVA的。通过思考后,于是我也自己写了一个小的WEB程序,通过java动态编译和自定义classloader也实现了一简易版本的在线JAVA编译小网页。项目地址为:https://gitee.com/puhaiyang/onlineJavaIde

预览图片:


实现热替换时的classloader代码和这个在线IDE差不多,就直接修改修改拿过来了,这个貌似更简单一点:

import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.nio.ByteBuffer;import java.nio.channels.Channels;import java.nio.channels.FileChannel;import java.nio.channels.WritableByteChannel;public class HotClassLoader extends ClassLoader {    public HotClassLoader() {        super(ClassLoader.getSystemClassLoader());    }    private File objFile;    public File getObjFile() {        return objFile;    }    public void setObjFile(File objFile) {        this.objFile = objFile;    }    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        //这个classLoader的主要方法        System.out.println("findClassfindClassfindClassfindClass");        Class clazz = null;        try {            byte[] data = getClassFileBytes(getObjFile());            clazz = defineClass(name, data, 0, data.length);//这个方法非常重要            if (null == clazz) {//如果在这个类加载器中都不能找到这个类的话,就真的找不到了            }        } catch (Exception e) {            e.printStackTrace();        }        return clazz;    }    /**     * 把CLASS文件转成BYTE     *     * @throws Exception     */    private byte[] getClassFileBytes(File file) throws Exception {        //采用NIO读取        FileInputStream fis = new FileInputStream(file);        FileChannel fileC = fis.getChannel();        ByteArrayOutputStream baos = new ByteArrayOutputStream();        WritableByteChannel outC = Channels.newChannel(baos);        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);        while (true) {            int i = fileC.read(buffer);            if (i == 0 || i == -1) {                break;            }            buffer.flip();            outC.write(buffer);            buffer.clear();        }        fis.close();        return baos.toByteArray();    }}

按照题目的要求,要写一个HelloMain类作为入口,我写的代码如下:

import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.io.File;import java.lang.reflect.Method;public class HelloMain {    private static Logger logger = LoggerFactory.getLogger(HelloMain.class);    private static MethodExcuteThread methodExcuteThread = new MethodExcuteThread();    private static ClassFileChangeListenerThread classFileChangeListenerThread = new ClassFileChangeListenerThread();    private static volatile Class desClazz;//共享变量    public static void main(String[] args) {        //创建两个线程,一个线程负责运行方法  另一个线程负责监听观察的文件是否有变动        /**启动类文件监听线程**/        classFileChangeListenerThread.start();        /**启动方法执行线程**/        methodExcuteThread.start();    }    private static class ClassFileChangeListenerThread extends Thread {        @Override        public void run() {            try {                File file = new File(HelloMain.class.getResource("").getFile() + "Worker.class");                long lastTime = file.lastModified();                boolean isFirst = true;                while (true) {                    Thread.sleep(2000);                    File newFile = new File(HelloMain.class.getResource("").getFile() + "Worker.class");                    long nowModified = newFile.lastModified();                    if (lastTime != nowModified) {                        logger.info("--->fileChanged(发现文件改变了):" + nowModified);                        lastTime = nowModified;                        reloadFile(newFile, methodExcuteThread);                    } else {                        if (isFirst) {                            logger.info("首次,也应该加载文件");                            reloadFile(newFile, methodExcuteThread);                            isFirst = false;                        } else {                            logger.debug("--->文件没有改变");                        }                    }                }            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    /**     * 方法执行线程     */    private static class MethodExcuteThread extends Thread {        volatile InheritableThreadLocal<Class> excuteClassLocal = new InheritableThreadLocal<>();        @Override        public void run() {            while (true) {                try {                    Class excuteClazz = desClazz;                    if (null == excuteClazz) {                        Thread.sleep(2000);                        System.out.println("还没有CLASS信息,[无法执行代码]");                        continue;                    }                    System.out.println("MethodExcuteThread   要执行代码了");                    Thread.sleep(1000);                    Object objObject = excuteClazz.getConstructor(new Class[]{}).newInstance(new Object[]{});                    Method excuteClazzMethod = excuteClazz.getMethod("doit", null);                    excuteClazzMethod.invoke(objObject, null);//执行                } catch (Exception e) {                    e.printStackTrace();                }            }        }        public InheritableThreadLocal<Class> getExcuteClassLocal() {            return excuteClassLocal;        }        public void setExcuteClassLocal(InheritableThreadLocal<Class> excuteClassLocal) {            this.excuteClassLocal = excuteClassLocal;        }    }    /**     * 重新加载FILE     * 在这里,将这个CLASS文件重新加载到内存中,从而替换掉之前的CLASS文件     * 即将之前那个类重新new一下     */    private static void reloadFile(File newFile, MethodExcuteThread methodExcuteThread) {        logger.debug("[reloadFile]");        HotClassLoader hotClassLoader = new HotClassLoader();        hotClassLoader.setObjFile(newFile);        try {            Class<?> objClass = hotClassLoader.findClass("com.haiyang.main.hotswitch.Worker");            //把这个新的CLASS设置到另一个线程中            methodExcuteThread.getExcuteClassLocal().set(objClass);//把新的class设置上            desClazz = objClass;        } catch (Exception e) {            e.printStackTrace();        }    }}

其主要思路是:创建两个线程和一个共享变量class

一个线程负责运行方法doit方法,通过反射去调用doit这个方法

另一个线程负责观察的文件是否有变动(通过最后修改日期来判断),如果有变动,就把新的class类加载过来,并把它赋值给共享变量


执行的worker类就随便写写:

public class Worker {    public Worker() {        System.out.println("<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了");    }    public void doit() {        System.out.println(this.getClass().getClassLoader().toString() + "--->----------------->666666  222" );    }}

然后运行下,入口HelloMain这个类,待启动好后,再把worker这个类的代码修改一下,在输出的值将222改成N个6,然后运行输出的控制台内容片段如下:


<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@46d156f9--->----------------->666666  222
MethodExcuteThread   要执行代码了
16:09:28.795 [Thread-1] DEBUG com.haiyang.main.hotswitch.HelloMain - --->文件没有改变
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@46d156f9--->----------------->666666  222
MethodExcuteThread   要执行代码了
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@46d156f9--->----------------->666666  222
MethodExcuteThread   要执行代码了
16:09:30.796 [Thread-1] INFO  com.haiyang.main.hotswitch.HelloMain - --->fileChanged(发现文件改变了):1507277369346
16:09:30.796 [Thread-1] DEBUG com.haiyang.main.hotswitch.HelloMain - [reloadFile]
findClassfindClassfindClassfindClass
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@46d156f9--->----------------->666666  222
MethodExcuteThread   要执行代码了
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@36f648f2--->----------------->666666  66666666
MethodExcuteThread   要执行代码了
16:09:32.797 [Thread-1] DEBUG com.haiyang.main.hotswitch.HelloMain - --->文件没有改变
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@36f648f2--->----------------->666666  66666666
MethodExcuteThread   要执行代码了
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@36f648f2--->----------------->666666  66666666
MethodExcuteThread   要执行代码了
16:09:34.797 [Thread-1] DEBUG com.haiyang.main.hotswitch.HelloMain - --->文件没有改变
<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了
com.haiyang.main.hotswitch.HotClassLoader@36f648f2--->----------------->666666  66666666


通过控制台输出可以得知,热替换功能已经成功了!但这个精简版的拿到实际场景中去实战还是有很大的问题的。拿来学习下还是可以的。

当然,记得让开发工具的编译状态为一直编译。设置如下:

[Settings]->[Build,Exe.......]->[Compiler]把Build project automatically把个勾,启动自动编译功能。如图: