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