JPDA(三):实现代码的HotSwap

JPDA系列:

1. JPDA(一):使用JDI写一个调试器

2. 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方法输出。下面介绍两种方式来完成,

  1. 使用Java Compiler API
  2. 使用Javassist

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的。

参考资料

时间: 2024-10-03 04:05:34

JPDA(三):实现代码的HotSwap的相关文章

Linux - Unix环境高级编程(第三版) 代码编译

Unix环境高级编程(第三版) 代码编译 本文地址:http://blog.csdn.net/caroline_wendy 时间:2014.10.2 1. 下载代码:http://www.apuebook.com/code3e.html 2. 安装依赖库:sudo apt-get install libbsd-dev  3. 进入下载目录make 4. 复制头文件和动态链接库 sudo cp ./include/apue.h /usr/include/ sudo cp ./lib/libapue

JAVA学习-第三个代码模型

第三个代码模型:对象比较 在讲解具体的概念之前,再来观察一种引用传递的形式,本类接收本类对象. 范例:观察程序代码(暂时不要去思考代码意义) class Person { private String name ; public Person(String name) { this.name = name ; }       // 接收本类对象          public void change(Person temp) {                    temp.name = "李

Bootstrap主要提供了三种代码风格

在Bootstrap主要提供了三种代码风格: 1.使用<code></code>来显示单行内联代码 2.使用<pre></pre>来显示多行块代码 3.使用<kbd></kbd>来显示用户输入代码 预编译版本的Bootstrap将代码的样式单独提取出来: 1.LESS版本,请查阅code.less文件 2.Sass版本,请查阅_code.scss文件 编译出来的CSS代码请查阅bootstrap.css文件第688行~第730行,由于

掌握java中的三种代码块的定义及使用

代码块概念 代码块本身并不是一个很难理解的概念,实际上之前也一直在使用.所谓代码块是指使用"{}"括起来的一段代码,根据位置不同,代码块可以分为四种:普通代码块.构造块.静态代码块.同步代码块,其中同步代码块本书将在多线程部分进行讲解,本章先来观察其他三种代码块. 普通代码块 直接定义在方法中的代码块称为普通代码块. public class CodeDemo01{ public static void main(String args[]){ {  // 普通代码块 int x =

Android三句代码使用沉浸式状态栏

用过android手机的人都知道android使用app的时候屏幕上方的状态栏都是黑色的,就算不是黑色的都与正在打开的app颜色不同.有一种灰常不搭调的感觉.~ 今天无意中看了一下关于沉浸式状态栏的资料~~作为强迫症重度患者怎能错过? 下面就开始使用沉浸式状态栏之旅: 代码未上图先行: 沉浸式: 非沉浸式: 嗯~虽然第二张图比较模糊,但是也可以想象第一张比较好看(- ̄▽ ̄)-. 那么接下来要说的就是如何实现第一张图的那样的效果: 首先要说明的是以下方法只适合android4.4或以上的系统 an

Scriplet的三种代码

Jsp中注释分为显示注释和隐式注释, 显示注释 -- 可以通过查看源代码看到 <!-- 第一种注释 -->  隐式注释 --  源代码中看不到 <%--jsp注释---%> <% //单行注释 /* 多行注释 */ %> Scriplet 表示脚本小程序,所有嵌入HTML中的java代码都必须使用Scriplet标记出来 scriplet表示有三种方法 <%%>    可以定义局部变量 <%!%>   定义全局变量,方法,类 <%=%>

宜信开源|Davinci一键部署:如何三句代码跑起Davinci

导读:之前喜欢Davinci的小伙伴儿在安装部署Davinci遇见问题时需要在github issue区等待技术人员的解答.现在不用怕啦,社区热心用户白菜君帮我们支持了docker-composer一键启动,以后只需寥寥几行代码,Davinci就能舒畅的run起来了.还等什么,赶紧部署起来吧~ 敲重点 Davinci Docker原部署教程在这里: https://github.com/edp963/davinci-docker 里面会不定时更新 记得收藏啊!! 下面是部署教程: 一.环境要求

作业三:代码规范

对于是否需要有代码规范,请考虑下列论点并反驳/支持: 1. 这些规范都是官僚制度下产生的浪费大家的编程时间.影响人们开发效率, 浪费时间的东西. 对于以上观点我是反对的 .如果说这些规范都是官僚制度产生的,那么更应该一丝不苟的执行,官僚制度,往大了说是法,应该无条件执行,往小了说是规范,可以帮助我们规范在打代码时自身不好的习惯.也许在编辑代码时,会比随意敲打耽误些许时间,但在检查错误时,规矩的编排格式,可以一目了然的看到自己的错误,为自己节省了更多的时间,会提高开发效率. 2. 我是个艺术家,手

软件工程P37习题三程序代码

//软件工程作业P37第三题   import java.util.Arrays;       //假设传入的数组是{how,are,you}   public class Test0001 {   public void theExchangeOfWords(String[] a){   for (int i = 0; i < a.length/2; i++) {   String temp;   temp=a[i];   a[i]=a[a.length-1-i];   a[a.length-