1. 程式人生 > >JPDA#3:實現程式碼的HotSwap

JPDA#3:實現程式碼的HotSwap

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.

也相當於是熱部署的功能了。下面是作為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的。

參考資料