自定义ClassLoader实现热替换

这几天学习了下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差不多,就直接修改修改拿过来了,这个貌似更简单一点:


 
 
  1. import java.io.ByteArrayOutputStream;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.Channels;
  6. import java.nio.channels.FileChannel;
  7. import java.nio.channels.WritableByteChannel;
  8. public class HotClassLoader extends ClassLoader {
  9. public HotClassLoader() {
  10. super(ClassLoader.getSystemClassLoader());
  11. }
  12. private File objFile;
  13. public File getObjFile() {
  14. return objFile;
  15. }
  16. public void setObjFile(File objFile) {
  17. this.objFile = objFile;
  18. }
  19. @Override
  20. protected Class<?> findClass(String name) throws ClassNotFoundException {
  21. //这个classLoader的主要方法
  22. System.out.println( "findClassfindClassfindClassfindClass");
  23. Class clazz = null;
  24. try {
  25. byte[] data = getClassFileBytes(getObjFile());
  26. clazz = defineClass(name, data, 0, data.length); //这个方法非常重要
  27. if ( null == clazz) { //如果在这个类加载器中都不能找到这个类的话,就真的找不到了
  28. }
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. }
  32. return clazz;
  33. }
  34. /**
  35. * 把CLASS文件转成BYTE
  36. *
  37. * @throws Exception
  38. */
  39. private byte[] getClassFileBytes(File file) throws Exception {
  40. //采用NIO读取
  41. FileInputStream fis = new FileInputStream(file);
  42. FileChannel fileC = fis.getChannel();
  43. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  44. WritableByteChannel outC = Channels.newChannel(baos);
  45. ByteBuffer buffer = ByteBuffer.allocateDirect( 1024);
  46. while ( true) {
  47. int i = fileC.read(buffer);
  48. if (i == 0 || i == - 1) {
  49. break;
  50. }
  51. buffer.flip();
  52. outC.write(buffer);
  53. buffer.clear();
  54. }
  55. fis.close();
  56. return baos.toByteArray();
  57. }
  58. }

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.Method;

public class HelloMain {
<span style="color:#cc7832;">private static </span>Logger <span style="color:#9876aa;"><em>logger </em></span>= LoggerFactory.<span style="font-style:italic;">getLogger</span>(HelloMain.<span style="color:#cc7832;">class</span>)<span style="color:#cc7832;">;


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();

}

<span style="color:#cc7832;">private static class </span>ClassFileChangeListenerThread <span style="color:#cc7832;">extends </span>Thread {
    <span style="color:#bbb529;">@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();
}

    }
}

<span style="color:#629755;"><em>/**

* 方法执行线程
*/
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();
}
}
}

    <span style="color:#cc7832;">public </span>InheritableThreadLocal&lt;Class&gt; <span style="color:#ffc66d;">getExcuteClassLocal</span>() {
        <span style="color:#cc7832;">return </span><span style="color:#9876aa;">excuteClassLocal</span><span style="color:#cc7832;">;

}

    <span style="color:#cc7832;">public void </span><span style="color:#ffc66d;">setExcuteClassLocal</span>(InheritableThreadLocal&lt;Class&gt; excuteClassLocal) {
        <span style="color:#cc7832;">this</span>.<span style="color:#9876aa;">excuteClassLocal </span>= excuteClassLocal<span style="color:#cc7832;">;

}
}

<span style="color:#629755;"><em>/**

* 重新加载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("<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>【重磅消息】我被构造了");
    }
<span style="color:#cc7832;">public void </span><span style="color:#ffc66d;">doit</span>() {
    System.<span style="color:#9876aa;"><em>out</em></span>.println(<span style="color:#cc7832;">this</span>.getClass().getClassLoader().toString() + <span style="color:#6a8759;">"---&gt;-----------------&gt;666666  222" </span>)<span style="color:#cc7832;">;


}
}

然后运行下,入口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把个勾,启动自动编译功能。如图:


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值