闲来无事试一下java的热修复。主要的原理是通过动态加载class文件的方式,参考文章为在 Java 中运用动态挂载实现 Bug 的热修复和动态替换目标进程的Java类
两篇文章分别在原理和实践上给出了详细的说明,这里做一下实例的整理并对遇到的问题和两篇文章中一些没有提到的注意事项进行一下说明
被替换的类的准备
先写一个要被替换掉的类libUtil,里面只有一个静态方法printString,打印一句话
public class libUtil {
public static void printString(String s){
System.out.println("this is a bug string : "+s);
}
}
然后是测试类,需要完成以下工作
1. 打印进程号,方便热修复时新建的虚拟机attach到这个进程上
2. 不停的调用要被替换掉的类的方法,以便我们可以清晰的看到热修复的效果
测试类如下:
import java.lang.management.ManagementFactory;
public class testHotSwap {
public static void main(String[] args) {
testStatic t=(newtestHotSwap()).new testStatic();
while(true){
try{
Thread.sleep(5000);
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println(name);
// get pid
String pid = name.split("@")[0];
System.out.println("Pid is:" + pid);
t.print();
}
catch(InterruptedException e){}
}
}
class testStatic{
public void print(){
libUtil.printString("did u miss me ?");
}
}
}
热修复的准备
接下来我们需要完成三件事:
1. 写一个进程去启动一个虚拟机并attach到已上线的进程上去
2. 写一个修复类去覆盖有问题的类
3. 写一个代理类,通过代理类的接口去实现热修复,其中这个代理类是要打包成jar文件的形式
首先来写一个修复类,这个修复类需要与原来的类在包名,类名,修饰符上完全一致,否则在classRedefine过程中会产生classname don’t match 的异常
代码如下:
public class libUtil {
public static void printString(String s){
System.out.println("this is a fixed string : "+s);
}
}
然后来写代理类,其中需要特别注意的有以下几点:
1. 必须含有static的代理main方法
2. 打成jar包后的Manifest.mf文件中必须含有以下两行:
l agent class: Agent-Class:testHotSwapDebug.debugJar
l Can-Redefine-Classes: true
Agent class指定了代理类的包名类名
Can redefineclass属性使得这个包可以对已载入的class进行redefine
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
public class debugJar {
public static void agentmain(String arg, Instrumentation inst)
throws Exception {
// only if header utility is on the classpath; otherwise,
// a class can be found within any classloader by iterating
// over the return value ofInstrumentation::getAllLoadedClasses
// 需要打成jar包并修改manifest.mf 添加一行指定agent class : Agent-Class:testHotSwapDebug.debugJar
//还需要加一行 :Can-Redefine-Classes: true
Class<?> libUtil = Class.forName("processWithBug.libUtil");
// copy the contents of typo.fix into abyte array
ByteArrayOutputStream output = new ByteArrayOutputStream();
InputStream input=null;
try {
input =new FileInputStream("C:\\Users\\Administrator\\workspace\\testHotSwapDebug\\bin\\processWithBug\\libUtil.class");//包名类名都要与原来的一致
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer)) != -1) {
output.write(buffer, 0, length);
}
}catch(Exception e){
if(input==null)
System.out.println("class input is null");
else
e.printStackTrace();
}
// Apply the redefinition
inst.redefineClasses(
new ClassDefinition(libUtil, output.toByteArray()));
}
}
接下来是用来attach到已有进程的类,注意事项有以下几点:
1. 需要在工程中引入tools.jar,这个jar包在jdk的lib文件夹下,但一般ide的运行环境为jre,所以需要特别添加进环境变量里面
2. 需要将attach.dll进行动态加载,同样attach.dll在jdk的一个bin目录下,需要拷贝到运行目录下,忘记具体在哪了,可以在jdk目录下进行搜索,找到以后放到jre/bin下就可以了
import java.io.File;
import com.sun.tools.attach.VirtualMachine;
public classtestFixBug {
static
{
System.loadLibrary("attach");
//需要将attach.dll放到java.library.path下
}
public static void main(String[] args) {
String processId = "17244";//需要修改为要被修复进程的pid
String jarFileName ="fixBug.jar";//有agentmain修复类的jar包名
File f=new File(jarFileName);
System.out.println(f.getAbsolutePath());
VirtualMachine virtualMachine;
try {
virtualMachine =VirtualMachine.attach(processId);
virtualMachine.loadAgent(jarFileName, "World!");
virtualMachine.detach();
} catch(Exception e){
e.printStackTrace();
}
}
}
运行结果
先运行要被替换的类的测试进程
11368@SKY-20150127GHW
Pid is:11368
this is a bugstring : did u miss me ?
运行attach的进程:
11368@SKY-20150127GHW
Pid is:11368
thisis a bug string : did u miss me ?
11368@SKY-20150127GHW
Pid is:11368
thisis a bug string : did u miss me ?
11368@SKY-20150127GHW
Pid is:11368
thisis a fixed string : did u miss me ?
11368@SKY-20150127GHW
Pid is:11368
this is a fixedstring : did u miss me ?
一些奇怪的小现象
当static方法直接在main中调用时不能被热修复,准确的说redefine成功了却没有效果。猜测是static块中调用static方法在编译时就已经固定了无法用动态重定义的方法产生影响