JPDA#3:實現程式碼的HotSwap
阿新 • • 發佈:2019-02-05
JPDA系列:
redefineClasses
下面直接上程式碼,我們的目標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.
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的。