自定义classloader实现JAVA热替换

这几天学习了下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把个勾,启动自动编译功能。如图:


  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水中加点糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值