很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到
java 领域后,这种理想也变成了现实,小如 IDEA 中更改页面就能马上生效,大如利用 Althas
工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为我已经为你准备好了
ClassLoader 甜点,Javassist 配菜,JavaAgent 高汤,手写插件加载器框架主食,外加 SPI
知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。
双亲委派模型
开始前,让我们先聊聊双亲委派这个话题,因为无论是做热部署,还是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示:
从如上图示,我们可以看到双亲委派模型整体的工作方式,整体讲解如下:
类加载器的 findClass (loadClass) 被调用
- 进入 App ClassLoader 中,先检查缓存中是否存在,如果存在,则直接返回
- 步骤 2 中的缓存中不存在,则被代理到父加载器,即 Extension ClassLoader
- 检查 Extension ClassLoader 缓存中是否存在
- 步骤 4 中的缓存中不存在,则被代理到父加载器,即 Bootstrap ClassLoader
- 检查 Bootstrap ClassLoader 缓存中是否存在
- 步骤 6 中的缓存中不存在,则从 Bootstrap ClassLoader 的类搜索路径下的文件中寻找,一般为 rt.jar 等,如果找不到,则抛出 ClassNotFound Exception
- Extension ClassLoader 会捕捉 ClassNotFound 错误,然后从 Extension ClassLoader 的类搜索路径下的文件中寻找,一般为环境变量 $JRE_HOME/lib/ext 路径下,如果也找不到,则抛出 ClassNotFound Exception
- App ClassLoader 会捕捉 ClassNotFound 错误,然后从 App ClassLoader 的类搜索路径下的文件中寻找,一般为环境变量 $CLASSPATH 路径下,如果找到,则将其读入字节数组,如果也找不到,则抛出 ClassNotFound Exception。如果找到,则 App ClassLoader 调用 defineClass () 方法。
通过上面的整体流程描述,是不是感觉双亲委派机制也不是那么难理解。本质就是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到 Bootstrap
加载器,如果 Bootstrap 加载器在缓存中也找不到,就抛错,然后这个错误再被一层层的捕捉,捕捉到错误后就查自己的类搜索路径,然后层层处理。
自定义 ClassLoader
了解了双亲委派机制后,那么如果要实现类的热更换或者是 jar 的热部署,就不得不涉及到自定义 ClassLoader 了,实际上其本质依旧是利用
ClassLoader 的这种双亲委派机制来进行操作的。遵循上面的流程,我们很容易的来实现利用自定义的 ClassLoader 来实现类的热交换功能:
public class CustomClassLoader extends ClassLoader {
//需要该类加载器直接加载的类文件的基目录
private String baseDir;
public CustomClassLoader(String baseDir, String[] classes) throws IOException {
super();
this.baseDir = baseDir;
loadClassByMe(classes);
}
private void loadClassByMe(String[] classes) throws IOException {
for (int i = 0; i < classes.length; i++) {
findClass(classes[i]);
}
}
/**
* 重写findclass方法
*
* 在ClassLoader中,loadClass方法先从缓存中找,缓存中没有,会代理给父类查找,如果父类中也找不到,就会调用此用户实现的findClass方法
*
* @param name
* @return
*/
@Override
protected Class findClass(String name) {
Class clazz = null;
StringBuffer stringBuffer = new StringBuffer(baseDir);
String className = name.replace('.', File.separatorChar) + ".class";
stringBuffer.append(File.separator + className);
File classF = new File(stringBuffer.toString());
try {
clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
} catch (IOException e) {
e.printStackTrace();
}
return clazz;
}
private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name, raw, 0, raw.length);
}
}
这里需要注意的是,在自定义的类加载器中,我们可以覆写 findClass,然后利用 defineClass 加载类并返回。
上面这段代码,我们就实现了一个最简单的自定义类加载器,但是能映射出双亲委派模型呢?
首先点开 ClassLoader 类,在里面翻到这个方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
如果对比着双亲委派模型来看,则 loadClass 方法对应之前提到的步骤 1-8,点进去 findLoadedClass 方法,可以看到底层实现是
native 的 native final Class<?> findLoadedClass0 方法,这个方法会从 JVM
缓存中进行数据查找。后面的分析方法类似。
而自定义类加载器中的 findClass 方法,则对应步骤 9:
clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
//省略部分逻辑
return defineClass(name, raw, 0, raw.length);
看看,整体是不是很清晰?
自定义类加载器实现类的热交换
写完自定义类加载器,来看看具体的用法吧,我们创建一个类,拥有如下内容:
package com.tw.client;
public class Foo {
public Foo() {
}
public void sayHello() {
System.out.println("hello world22222! (version 11)");
}
}
顾名思义,此类只要调用 sayHello 方法,便会打印出 hello world22222! (version 11) 出来。
热交换处理过程如下:
public static void main(String[] args) throws Exception {
while (true) {
run();
Thread.sleep(1000);
}
}
/**
* ClassLoader用来加载class类文件的,实现类的热替换
* 注意,需要在swap目录下,一层层建立目录com/tw/client/,然后将Foo.class放进去
* @throws Exception
*/
public static void run() throws Exception {
CustomClassLoader customClassLoader = new CustomClassLoader("swap", new String[]{"com.tw.client.Foo"});
Class clazz = customClassLoader.loadClass("com.tw.client.Foo");
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod("sayHello", new Class[]{});
method.invoke(foo, new Object[]{});
}
当我们运行起来后,我们会将提前准备好的另一个 Foo.class 来替换当前这个,来看看结果吧(直接将新的 Foo.class 类拷贝过去覆盖即可):
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello w