JPDA系列:
redefineClasses
JPDA提供了一个API,VirtualMachine#redefineClasses,我们可以通过这个API来实现Java代码的热替换。
下面直接上代码,我们的目标VM运行了如下代码,前面已经说过,目标VM启动时需要添加option,-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8787
public class Main {
public static void main(String[] args) throws Exception {
Random random = new Random();
while(true) {
int i = random.nextInt(1000);
if(i % 10 == 0) {
new Foo().bar();
Thread.sleep(5000);
}
}
}
}
public class Foo {
public void bar() {
System.out.println("hello Foo.");
}
}
我们要实现的代码Hot Swap就是,直接在线修改
Foo#bar
方法,使该方法输出
hello HotSwapper.
也相当于是热部署的功能了。下面是作为debugger的HotSwapper的代码,import com.sun.jdi.Bootstrap;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.Connector;
import com.sun.tools.jdi.SocketAttachingConnector;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class HotSwapper {
public static void main(String[] args) throws Exception{
List<Connector> connectors =
Bootstrap.virtualMachineManager().allConnectors();
SocketAttachingConnector sac = null;
for (Connector connector : connectors) {
if(connector instanceof SocketAttachingConnector) {
sac = (SocketAttachingConnector)connector;
}
}
if(sac != null) {
Map<String, Connector.Argument> defaultArguments = sac.defaultArguments();
Connector.Argument hostArg = defaultArguments.get("hostname");
Connector.Argument portArg = defaultArguments.get("port");
hostArg.setValue("localhost");
portArg.setValue("8787");
VirtualMachine vm = sac.attach(defaultArguments);
List<ReferenceType> rtList = vm.classesByName("me.kisimple.just4fun.Foo");
ReferenceType rt = rtList.get(0);
Map<ReferenceType, byte[]> newByteCodeMap = new HashMap<ReferenceType, byte[]>(1);
byte[] newByteCode = genNewByteCode();
newByteCodeMap.put(rt, newByteCode);
if(vm.canRedefineClasses()) {
vm.redefineClasses(newByteCodeMap);
}
}
}
}
要使用VirtualMachine#redefineClasses
方法,需要拿到要替换的Java类的字节码,由栗子中的genNewByteCode
方法输出。下面介绍两种方式来完成,
JavaCompiler
Java Compiler API 使用方式如下,
private static byte[] genNewByteCodeUsingJavaCompiler() throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// compiler.run(null, null, null, "E:\\Projects\\just4fun\\src\\main\\java\\me\\kisimple\\just4fun\\Foo.java");
File javaFile =
new File("E:\\Projects\\just4fun\\src\\main\\java\\me\\kisimple\\just4fun\\Foo.java");
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> compilationUnit =
fileManager.getJavaFileObjectsFromFiles(Arrays.asList(javaFile));
compiler.getTask(null, fileManager, null, null, null, compilationUnit).call();
File classFile =
new File("E:\\Projects\\just4fun\\src\\main\\java\\me\\kisimple\\just4fun\\Foo.class");
InputStream in = new FileInputStream(classFile);
byte[] buf = new byte[(int)classFile.length()];
while (in.read(buf) != -1) {}
return buf;
}
使用这种方式我们需要先修改Foo的源码,
public class Foo {
public void bar() {
System.out.println("hello HotSwapper.");
}
}
然后运行HotSwapper就会使用 JavaCompiler 将修改后的源码重新编译,生成新的Foo.class文件,再使用文件IO的API读入class文件就达到我们的目的了。然后我们就可以看到目标VM的输出如下,
Listening for transport dt_socket at address: 8787
hello Foo.
hello Foo.
hello Foo.
Listening for transport dt_socket at address: 8787
hello HotSwapper.
hello HotSwapper.
妥妥的实现了代码的Hot Swap,或者说是热部署。
在将class文件读入到字节数组时,有个地方需要注意一下,
byte[] buf = new byte[(int)classFile.length()];
字节数组的大小不可以随便定义,不然会出现以下错误,目标VM会误以为整个字节数组都是class文件的字节码,Exception in thread "main" java.lang.ClassFormatError: class not in class file format
at com.sun.tools.jdi.VirtualMachineImpl.redefineClasses(VirtualMachineImpl.java:321)
Javassist
Javassist的API使用起来要简单得多,
private static byte[] genNewByteCodeUsingJavassist() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("me.kisimple.just4fun.Foo");
CtMethod cm = cc.getDeclaredMethod("bar");
cm.setBody("{System.out.println(\"hello HotSwapper.\");}");
return cc.toBytecode();
}
使用这种方式我们也不需要去修改Foo的源文件。
HotSwapper
其实在Javassist中已经实现了一个HotSwapper了,通过源码也能看到,它也是使用了JPDA的API来实现Hot Swap的。