一:小实验
让我们做一个有趣的小实验:在程序运行的过程中改变class文件会改变运行结果吗?
- 先创建一个User对象,打印我是1.0版本
public class User {
public void add(){
System.out.println("哈哈哈,我是User 1.0版本");
}
}
复制类到任一文件目录下,将打印信息改成2.0版本
public class User {
public void add(){
System.out.println("哈哈哈,我是User 2.0版本");
}
}
- 创建HotSwap类
public class HotSwap {
public static void main(String[] args) throws InterruptedException {
//user 1.0版本
User user1 = new User();
user1.add();
}
}
运行,此时得到是1.0版本的User.class,可以使用反编译工具进行验证
- 找到2.0版本的User.java ,使用命令javac User.java 运行,得到2.0版本的User.class, 此时将class文件先复制到剪切板上,也可以用反编译工具验证当前类的版本
- 在HotSwap类中继续添加以下代码
//java 2.0版本,在这个时候将User.class 2.0版本复盖User.class 1.0版本
Thread.sleep(10*1000);
//再次调用add方法
User user2 = new User();
user2.add();
其中你要在线程停止的10秒内将2.0版的User.class 覆盖掉1.0版的User.class
-
运行结果
程序先打印出了哈哈哈,我是User 1.0版本
,将2.0版本的class文件复盖1.0版本的后依然打印哈哈哈,我是User 1.0版本
,那既然class文件被覆盖掉了为什么还是打印1.0版本呢?
-
思考原因
底层只做了一次classLoader,字节码文件已经被加载到了JVM内存当中,并不能察觉到字节码文件的更新,而解决此问题的办法就是重新加载运行一遍即可,这也解释了为社么我们做服务端代码开发时如有代码变动就需要重新启动服务器进行加载的原因 -
maven工程中的一个问题
如果我们将target文件夹中的字节码文件删除掉此时程序还能正常运行吗?
答案是肯定不能的,我们可以通过命令mvn clean package
来解决
二:功能实现
- 定义类MyClassLoader 注意一定要继承 ClassLoader方法 ,并重写findClass方法
public class MyClassLoader extends ClassLoader {
//重写加载class文件方法,并传递class文件路径
@Override
public Class<?> findClass(String name) {
// 文件名称
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 获取文件输入流
InputStream is = this.getClass().getResourceAsStream(fileName);
// 读取字节
byte[] b = new byte[0];
try {
b = new byte[is.available()];
is.read(b);
} catch (IOException e) {
e.printStackTrace();
}
// 将byte字节流解析成jvm能够识别的Class对象
return defineClass(name, b, 0, b.length);
}
}
- 测试
使用上面已经定义过的热部署入口HotSwap类 ,重新编写代码如下
/**
* @Auther: 洺润Star
* @Date: 2019/12/15 14:47
* @Description:热部署入口
*/
public class HotSwap {
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException,
SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException {
loadUser();
System.gc();
Thread.sleep(1000);// 等待资源回收
// 需要被热部署的class文件(2.0版本)
File file1 = new File("F:\\User.class");
// 之前编译好的class文件(1.0版本)
File file2 = new File(
"D:\\Users\\Administrator\\IdeaProjects\\my_test\\target\\classes\\ant_jvm\\class_loader\\test\\User.class");
boolean isDelete = file2.delete();// 删除旧版本的class文件
if (!isDelete) {
System.out.println("热部署失败.");
return;
}
file1.renameTo(file2);
System.out.println("update success!");
loadUser();
}
public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException,
NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
MyClassLoader myLoader = new MyClassLoader();
Class<?> class1 = myLoader.findClass("ant_jvm.class_loader.test.User");
Object obj1 = class1.newInstance();
Method method = class1.getMethod("add");
method.invoke(obj1);
System.out.println(obj1.getClass());
System.out.println(obj1.getClass().getClassLoader());
}
}
相比于之前的小实验,这里的对象使用ClassLoader创建,而非之前的new创建,main方法中主要是先将原来的字节码文件删除掉,然后将需要热部署的字节码文件移动到相应位置,运行结果如下(乱码不影响结论)
可以看到由于执行了两次loadUser()方法,已经成功的使用了User.class 2.0版本的字节码版本