> 技能 > 类加载器与类的热替换(Hotswap) 在 Java 应用开发中,经常会有的一个问题,需要在应用正在运行的过程中,把一个新实现的功能加到 class 中,并且不能停止当前的应用运行。也就是说,要实现类的热替换(Hotswap)。这一点,在 Java Web 应用中体现的更为明显,每次更新配置,更新 class,都需要重新启动服务器,重新加载这些改动,这一过程相当耗时。
那为什么不在应用服务器运行时,或者说 Java 的应用运行过程中,把 class 替换后实时生效呢?
这一点,是和 Class 的生命周期密切相关的。
任何一个使用到的 Class,都需要被 classLoader 加载到 JVM 中,这个加载的过程,又分为
- defineClass
- loadClass
- resolveClass
即根据提供的不同形式的 class 文件的内容,转化成一个 Class,对其进行加载,链接。
而所谓的 hotswap,就是使用新的代码替换掉已经加载的这个 Class 中的内容。
我们这里使用 Javassist 来模拟类似的效果,通过动态的修改 class 内某个方法的内容,来实现热替换的效果。
基本步骤:
先定义一个接口,包含一个方法,该方法内的内容在后面用于热替换
定义一个接口的实现
在 Servlet 内接收页面上传入的参数,页面上的参数内容,则是具体上面接口定义方法及方法对应的方法体内容
在接收到参数后,根据参数内容,使用 Javassist 进行热替换的操作。
基本 Javassist 的代码大概是下面的样子:
ITemp test = new Temp();
ClassPool pool = new ClassPool(true);
private ITemp hotswap(String str) {
String methodBody = null;
try {
pool.insertClassPath(new ClassClassPath(ITemp.class));
String className = "ITemp_" + UUID.randomUUID();
CtClass clazz = pool.makeClass(className);
clazz.setInterfaces(new CtClass[]{pool.get("com.xxxx.ITemp")});
methodBody = str;
clazz.addMethod(CtMethod.make(methodBody, clazz));
ITemp fun = (ITemp) clazz.toClass().newInstance();
calculatorClass.writeFile("d:/temp");
return fun;
} catch(Exception e) {
}
在用的过程中,接收到参数后,把 hotswap 方法返回的实例再指给 test 这个实例。然后 test 内对应方法的执行已经改成在页面上传入的逻辑。
原理
上面的是怎么样实现的呢?
其实,上面的替换,实质上,是重新生成了一个 ITemp 这个接口的实现类,然后我们在页面上定义的,是这个实现类的方法中的内容。然后我们的替换,是把一个实现类的引用改成另一个。
这时,一定有同学提起 JDK 默认支持的 Agent,这种基于 JVMTI 的实现。但是,这里需要注意的是,此时使用的是 Agent,如果使用其 pre_main,那么支持的是 Class 的 defineClass 执行前,对其进行所谓的增强或者是织入,类似于 OpenJPA 的 Enhance 和 AJP 的 weave。
这种相当于是把 class 内容在读到内容之后修改了一下,然后传给 classLoader 进行 defineClass。
而 JVMTI 在 JDK1.6 之后,支持的另外一种形式,是在应用已经部署启动之后,要进行类的改变的,使用的是 agent_main 方法,此时,需要对 class 进行retransform。像 Java 里一些常用的 Profiler 工具使用的就是这种,attach 到已经启动的 Java 进程,对其进行 retransform,例如 Btrace。
这里的 transform 会调用 Instrumention 的 RedefineClass,本质上是修改类的定义。我们以 Btrace 为例,来看他在背后做了什么。