这几天学习了下JVM的原理,在看一个视频教程,上面上一个这样的题目:
1. 实现热替换。
运行一个程序HelloMain,他会循环调用另外一个类Worker.doit()方法。此时,对Worker.doit()方法做更新。要求 更新后,HelloMain可以发现新的版本。
可以选择替换class文件 ,也可以选择替换jar包。
对于这个题目,让我想起了之前在公司的项目中,有时修复了一个小的BUG,修改的JAVA文件,但为了不重启服务器,节约时间,就之间拿着本地开发环境编译好的CLASS文件,把它放在远程服务器上的tomcat的WEBAPPS的相关项目目录下就可以了。当时是见我们的主管这样做的,第一次看见时觉得很神奇,那时的我还以为所有的JAVA容器都是这样的,只要把calss替换掉,它就会自动对新的类进行替换。通过这几天的学习才知道,原来TOMCAT能达到那样的效果是因为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();
}
}
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 InheritableThreadLocalexcuteClassLocal = 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 getExcuteClassLocal() { return excuteClassLocal; } public void setExcuteClassLocal(InheritableThreadLocal 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把个勾,启动自动编译功能。如图: