系统调用一个class,比如xx.class去查看xx类的信息的时候:
1,loadClass,先在loadedClass里找,找不到,就交给父类去load试试,如果父类还是找不到(返回null),就去调用findClass试试
2,findClass去找未加载进内存的.class方法,如果找到了,就做defineClass的步骤,把它加载进内存
3,defineClass本身是一个native方法,在这个native方法里,当解析到方法的父类的时候(每个方法都有父类,要么是自定义的,要么是Object),会回调当前classLoader的loadClass方法,然后loadClass方法会重复1-3步骤,寻找到父类以后,把父类交出去。
按照我的理解,defineClass里所做的把.class文件转化成loadedClass的步骤,就包括了类加载生命周期里的加载,验证,解析步骤
其中findClass里去寻找.class文件(或者其他的二进制流的方法)属于加载的第一步,“1,通过一个类的全限定名来获取定义此类的二进制流。 ”
defineClass这个native属于加载的第二步“2,把字节流所代表的静态存储结构转化为方法区的运行时数据结构”到这里都属于类加载中的加载阶段。
在这个过程中,defineClass回调当前类的loadClass方法来寻找被加载类的父类,这个过程是验证and解析的过程,属于类加载中的连接(验证,准备,解析)阶段。这里找到父类以及其他相关的各种引用的类之后,就已经把符号引用转化为直接引用了,所以到这里递归完以后,连接阶段就结束了
也就是说,在我们看来,调用defineClass的时候,等于把类给初始化了,类的加载,连接(验证,准备,解析)阶段+初始化等,都执行了。
绕回来,代码
这里做了一个类的热编译+热加载的功能
这里分两种情况,
1,代码是main方法运行的情况下(非web应用),默认的类加载器是appClassLoader,这种情况下,我们写的.java文件会被编译成.class文件,去进行加载(可以直观的看见和替换.class文件)
2,代码是web应用运行的情况下(以spring为例),默认的类加载器是WebAppClassLoader,这里的.java文件,在编译成.class文件之后,会再被编译成.jar文件,放置在web目录下的web-inf/lib里(不能直观的看见和替换.class文件)
但是不影响我们加载类的代码书写
流程大致如下:
1,得到文件转换来的二进制流
2,把二进制流给自定义的类加载器
3,调用自定义类加载器的loadClass方法,输入对应名字,
4,复写loadClass方法,做出判断,if name == 我要加载的类, 拿出对应的二进制流,defineClass
else 交给对应的加载器去加载。
因为defineClass会回调loadClass方法,所以一定要复写loadClass方法,不然很容易就会报错,比如找不到需要被加载的父类之类的。(虽然报错了也不影响实际使用)
public class HowswapClassLoader extends ClassLoader {
private Map files = new HashMap<>();
public HowswapClassLoader(Map files) {
this.files.putAll(files);
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] buf = getByte(files.get(name));
if (buf == null) {
return HowswapClassLoader.class.getClassLoader().loadClass(name);
}
files.remove(name);
return defineClass(name, buf, 0, buf.length);
}
@Override
public Class loadClass(String name) throws ClassNotFoundException {
return findClass(name);
}
}
rootPath是临时文件夹就行了,用来存放编译后的.class文件,
比如 System.getProperty(“user.dir”) + “\” + “temp\”;
public class XX{
private static void compilerJavaFiles(List files,String rootPath) {
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
Iterable options = buildOptions(rootPath);
StandardJavaFileManager fileManager = javaCompiler
.getStandardFileManager(null, null, null);
Iterable extends JavaFileObject> compilationUnits = fileManager
.getJavaFileObjectsFromFiles(files);
javaCompiler.getTask(null, fileManager, null, options,
null, compilationUnits).call();
}
private static List buildOptions(String rootPath) {
List options = new ArrayList<>();
options.add("-encoding");
options.add("UTF-8");
//鉴于很多编译文件都需要引用到一些二方三方包,(非java.xx的包),这里通过直接找urls的方式,一次性取出所有编译时候可能用到的classpath,供使用。
options.add("-classpath");
options.add(getClasspath());
//编译目录是root目录,-d表示在根目录下创建(如果没有)package对应的文件夹,然后把.class文件放到对应的package文件夹下
options.add("-d");
options.add(rootPath);
return options;
}
private static String getClasspath() {
StringBuilder sb = new StringBuilder();
URL[] urls = ((URLClassLoader)XX.class.getClassLoader()).getURLs();
int length = urls.length;
for (int i = 0; i < length; ++i) {
URL url = urls[i];
String p = url.getFile();
sb.append(p).append(File.pathSeparator);
}
return sb.toString();
}
}
类的热编译+热加载的功能
1,找到类.java的存放点,
2,读取类.java文件,通过XX.compilerJavaFiles()方法,得到编译后的.class文件
3,把.class文件给HowswapClassLoader去加载进内存
4,通过HowswapClassLoader去获取类(Class