之前android项目里面用到了微信的Tinker热修复框架,果断不能知其然,而不知其所以然啊,于是就一番源码看下去,发现其中很多都是关于 ClassLoader有关,想起刚13年毕业那会写了一个关于ClassLoader小demo,于是依照热修复的思路,看能不能扩存成一个java的热部署功能。花了小半天的时间,总算是搞定,虽不尽完美,但是还是想写到博客上,算作一个小案例。
一个java程序,由无数个class文件组成,当程序运行时,则会调用主函数入口,开始运行相关的功能,这些功能代码都会被封装在不同的class文件中,当然不会一股脑的全部加载所有的class文件,根据类链接,依次通过ClassLoader 加载到内存中,而只有加载到内存中,才能被其他class所引用,完成相应的功能。
ClassLoader的生命周期依次为 :加载-》验证-》准备-》解析-》初始化-》使用-》卸载。
java提供两种类加载器,一为 Java虚拟机自带的加载器,二为 用户自定义的类加载器。
Java虚拟机自带的加载器
• 根类加载器( Bootstrap,使用 c++ 编写,无法在 Java 代码中得到该类)
• 扩展类加载器( Extension,使用 Java 实现)
• 系统类加载器( System,应用加载器,使用Java代码实现)
java.lang.ClassLoader 的子类
• 用户可以定制类的加载方式,由其中defineClass 函数,加载指定的calss流。
ClassLoader使用的是双亲委托机制来实现类的加载,每一个ClassLoader都有一个父类ClassLoader引用,不是继承关系,只是引用关系,当一个ClassLoader需要加载某个类的时候,会在loadClass函数中判断,当前加载器是否加载过这个类,如果没有,则让父加载器去搜索是否加载过,如果有则返回,如果还没有,则在往上抛,一直到Bootstrap ClassLoader 开始试图加载class,先从JDK的核心类库中加载所需要的class文件,如果系统中加载不到,则到Java的扩展类库中加载所需要的class文件,如果还没有,则去App ClassLoader 进行加载,如果它也没有加载得到的话,在由委托发起者,自己去加载本地或者网络上的class文件。如果都没有加载成功,则会抛出ClassNotFoundException异常。
(图片来自csdnjobinZhang)
为什么要使用双亲委托机制?因为可以避免类的重复加载,以及类的安全性,如果用户随便定义一个 Integer 类让ClassLoader加载顶替了java api 中Integer类,就会引发很大的安全问题。而双亲委托机制就很好的解决了这一问题,当一个类被顶层ClassLoader加载完成之后,就不会被顶替加载,就保证了安全性。
既然了解了ClassLoader的加载原理,那我们就可以自己加载指定的class文件,来实现代码的热部署功能。
首先代码分三部分
1、自定义ClassLoader部分,用于加载热部署的class文件。
2、注解IOC注入功能,用于注入class对象。
3、监听热部署class文件目录,如果目录发生变化,则通过自定义ClassLoader重新加载。
Service层用于模拟热部署的业务层,精力有限,目前就不使用微信的Tinker热修复框架那样通过二进制文件流比对整个工程制作差分包,然后在合成一个新的工程,这里只通过一部分的代码来演示热部署功能点。
话不多说,上代码
package cn.app.wuzhi.classload;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String baseDir;
private String clazzName;
public MyClassLoader() {
}
/**
* 指定文件 读取byte 数组
*/
public byte[] readClassFile(String filename) {
BufferedInputStream bufferedInput = null;
ByteArrayOutputStream bytesArray = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
try {
bufferedInput = new BufferedInputStream(new FileInputStream(filename));
int bytesRead = 0;
while ((bytesRead = bufferedInput.read(buffer)) != -1) {
bytesArray.write(buffer, 0, bytesRead);
}
} catch (FileNotFoundException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
} finally {
//关闭 BufferedInputStream
try {
if (bufferedInput != null)
bufferedInput.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return bytesArray.toByteArray();
}
/**
* 加载class
*/
@Override
public Class> findClass(String name) throws ClassNotFoundException {
String classFile = getClassPath();
//本地 读取class 文件字节流
byte[] classbyte = readClassFile(classFile);
//defineClass 函数 允许用户以byte 数组的方式加载 calss 对象
Class clazz = defineClass(name, classbyte, 0, classbyte.length);
return clazz;
}
public MyClassLoader(String baseDir) {
this.baseDir = baseDir;
}
public String getBaseDir() {
return baseDir;
}
public void setBaseDir(String baseDir) {
this.baseDir = baseDir;
}
public void setClazzName(String clazzName) {
this.clazzName = clazzName;
}
public MyClassLoader(String baseDir, String clazzName) {
this.baseDir = baseDir;
this.clazzName = clazzName;
}
public String getClassPath() {
return baseDir +"/"+clazzName;
}
}
这一块代码就不贴了,用的是NIO的WatchService,网上代码讲解得也非常详细。
首先在Service上标记一个注解,定义一个name,作为此Service的唯一标识。IOC注入时,以此为标识查找。
IocFactory 类用于对象注入,代码不多。
package cn.app.wuzhi.iocinjection;
import cn.app.wuzhi.IocTest;
import cn.app.wuzhi.classload.MyClassLoader;
import cn.app.wuzhi.filechange.FileListener;
import cn.app.wuzhi.filechange.FileWatcher;
import cn.app.wuzhi.iocinjection.annotation.AotoWrite;
import cn.app.wuzhi.iocinjection.annotation.Service;
import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
public class IocFactory {
//工厂主目录
private String baseDir = "D:/it.work/classLoaderTest/out/production/classLoaderTest";
// service 包名
private String servicePackage = "cn.app.wuzhi.service.impl";
//热部署测试目录
private String serviceDir = baseDir + "/" + servicePackage.replace(".", "/");
//补丁目录
private String patchDir = "D:/it.work/classLoaderTest/patch";
private static IocFactory singleton = null;
//记录通过class newInstance构造出来的所有对象
private HashMap servicesMap = new HashMap<>();
//使用ioc注入功能的对象
private CopyOnWriteArrayList
IOC测试使用,这边以Shop、Consumer 类演示热部署功能。被自定义ClassLoader加载出来的类,由于和工程中的类,全限定名称一致,但是,由于不是同一个ClassLoader加载出来,不能作为同一个类型的对象,于是我们用一个接口作为引用。
在test函数中,我们每隔一秒钟调用shop对象 的 helloShop函数。
工程中的Shop类 修改之后作为补丁的Shop类
重新编译工程,把编译好的ShopService.class 复制出来,先运行工程,然后在拷贝到补丁目录,然后在删除,控制台打印如下。
至此一个热部署的功能点依然完成,当然其中还有很多问题。不过学习就是要这样一点 一点的进步,一天一天的努力,持之以恒才能有所收获。