Java热替换(一)

本文介绍了Java热替换的概念,讲解了java agent、attach API和instrumentation在热替换中的作用。通过示例代码展示了如何实现一个简单的热替换程序,但指出这种方法仅能替换方法体内的代码,对于方法增删会导致替换失败,这是Java的一个已知限制。文章最后提到了dcevm和jrebel等工具作为更完善的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

离开学校来广州上班已经3年+了,还记得才进公司的愣头青样子,思维还停留在学校,不懂得一份工作所意味着的责任。大条的我搞出了很多问题,都是老人帮忙擦屁股(嘿嘿嘿)。加班很多,也经常通宵,很累但仗着精力旺盛,熬过来然后习惯了知道现在。

回到正题

熟悉了业务后,工作变得重复,多出的时间便找项目里各个角落的代码来看。其中一个比较有意思的工具便是今天的主题——热替换工具。

工具主要涉及到3个java点:agent、attach api、instrumentation,我们一一道来。

(1)java agent

简单说,java程序的入口为main函数,而agent可以在main函数执行先执行一段代码,函数名叫premain或者agentmain。示例代码很简单,如下:

首先,新建agent类,定义一个简单的premain函数

import java.lang.instrument.Instrumentation;

public class HelloAgent {
    public static void premain(String agentArgument, Instrumentation instrumentation){
        System.out.println("hello agent");
    }
} 

然后,把这个agent类打成jar包,这里我们保存为helloagent.jar,jar包的MANIFEST.MF文件如下:

Manifest-Version: 1.0
Premain-Class: hotreplace.helloagent.HelloAgent

最后,写个最简单的helloworld程序如下

public class TestHelloAgent {
    public static void main(String[] args){
        System.out.println("hello world");
    }
}

加启动参数

-javaagent:helloagent.jar

点击运行

hello agent
hello world

(2)attach api

我们先试想一下,如果要实现上面的helloagent,需要在启动main函数之前,就配好对应agent的启动参数,这样问题来了,假如我要实现如“热替换”的操作,我怎么能在java程序启动前知道要替换的class文件。(因为问题总是临时的,线上运行着的服务器一个功能突然出现问题,对应的class文件需要替换)

于是,attach技术便为了实现这个需求出现了

同样的,我们需要建1个agent类,不过上面的premain函数要改为agentmain

import java.lang.instrument.Instrumentation;

public class HelloAttach {
    public static void agentmain(String agentArgument, Instrumentation instrumentation){
        System.out.println("hello agent");
    }
}

打jar包的MANIFEST.MF文件也需要做改动,即Premain-Class改为Agent-Class,然后保存为helloattach.jar

Manifest-Version: 1.0
Agent-Class: hotreplace.helloattach.HelloAttach

然后,主程序如下

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class TestHelloAttach {
    public static void main(String[] args){
        final String vmName = "hotreplace.helloattach.TestHelloAttach";
        final String agentPath = "./helloattach.jar";
        VirtualMachineDescriptor vmdTar = null;
        for(VirtualMachineDescriptor vmd : VirtualMachine.list()){
            if(vmd.displayName().equals(vmName)){
                vmdTar = vmd;
                System.out.println("found the vm");
                break;
            }
        }
        if(vmdTar !=  null){
            try {
                VirtualMachine vm = VirtualMachine.attach(vmdTar);
                vm.loadAgent(agentPath);
                System.out.println("attached");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        System.out.println("hello world");
    }
}

多数情况,主程序和attach程序是分开的,这里为了偷懒我写在一起了,嘿嘿嘿。运行结果如下:

found the vm
hello agent
attached
hello world

(补充:上面的程序需要引用的jdk_xxx/lib/tools.jar)
简单解释一下上面程序,VirtualMachine类可以拿到这台机子的所有jvm进程,然后对比进程名拿到想要的进程,再通过loadAgent函数,把我们之前打好的agent包attach到对应进程

现在,我们知道了怎么在一个运行的jvm中插入自己想要执行的代码,那么要实现热替换我们还需要些什么?

(3)instrumentation

先看段代码

public class ReplaceAgent {
    public static void agentmain(String agentArgument, Instrumentation instrumentation){
        System.out.println("start agent");
        Params params = parseParams(agentArgument);
        if(params == null){
            System.out.println(String.format("wrong agentArgument=%s", agentArgument));
            return;
        }
        instrumentation.addTransformer(new SimpleTransformer(params.filePath, params.className), true);
        try {
            instrumentation.retransformClasses(TestClass.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("end agent");
    }

可见这个instrumentation这个参数是有用的。。。其中最重要的就是这个transformer类,这个transformer可以帮我们替换想要的class文件,如果想问为什么,我也不知道。。。没看过其底层代码,感兴趣可以去研究下。示例代码如下:

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

class SimpleTransformer implements ClassFileTransformer{
    private final String filePath;
    private final String className;

    SimpleTransformer(String filePath, String className) {
        super();
        this.filePath = filePath;
        this.className = className;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(!className.equals(this.className))
            return null;
        System.out.println(String.format("ready to replace class=%s", className));
        return SimpleHelper.file2bytes(filePath);
    }
}

代码中的SimpleHelper.file2bytes()是一个简单的读class文件函数,为了偷懒写得不规范,还是硬着头皮贴出来。

class SimpleHelper {
    static byte[] file2bytes(String filePath){
        File file = new File(filePath);
        long len = file.length();
        try(FileInputStream fis = new FileInputStream(file)) {
            byte[] bs = new byte[(int)len];
            int num = fis.read(bs);
            System.out.println(String.format("read num=%d", num));
            return bs;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[0];
        }
    }
}

其它和1/2步一样操作不赘述了,脑补一哈。不一样的是jar包的MANIFEST.MF文件(强调:后面的空格是必须的,可以不加试试。。。)

Manifest-Version: 1.0
Agent-Class: hotreplace.simplereplace.agent.ReplaceAgent
Can-Retransform-CLasses: true
Can-Redefine-classes: true

测试用例:

public class TestClass {
    public void hello(){
        System.out.println("hello 1");
    }
}
public class TestCase {
    public static void main(String[] args){
        while(true){
            try {
                TestClass clazz = new TestClass();
                clazz.hello();
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

为了偷懒,我直接把TestClass 的class文件单独提出保存在1目录,作为我们替换TestClass类的目标文件,然后更改TestClass的内容为“hello 2”。运行我们执行替换的主程序(如第2步的TestHelloAttach )

执行结果:

hello 2
hello 2
hello 2
start agent
ready to replace class=hotreplace/simplereplace/test/TestClass
read num=533
end agent
hello 1
hello 1

这样,我们完整的热替换程序就完成了,完整代码见:

http://download.youkuaiyun.com/detail/gonnaflynow/9689356

回过头来看,代码很简单,但写起来细节还是挺多的,可以自己从头写体会一哈。

这段代码的缺点:只能实现“方法体内的代码替换”,怎么解释呢,就是假如你在更改的java文件中新增了一个方法,或者删除了一个方法,都会替换失败。有过eclipse开发经历的应该会体会到这点。这个是由jvm本身所限制的,直到现在都是java一个优先级较高的bug。

这个限制让我们实现的这个热替换工具显得“鸡肋”了。(学习目的达到了。。。)

但是,世界上牛人这么多,难道就没人先一步sun工程师解决这个问题吗。后面我们要讨论的dcevm和jrebel工具便是牛人的杰作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值