JavaAgent无法替换运行中的字节码

——————*脑袋空空,口袋空空 q (^(oo)^) P ——————

这是一个关于JavaAgent的故事…

如何在线修复bug? 其中的一个方法就是,通过JavaAgent

这是个很大的问题,因为没有给出前提,很多情况下,为了避免降低用户体验,我们更多的是想不重启应用的情况下把有问题的代码修复掉,可是很矛盾的一点是,任何在生产环境的改动都必须是谨慎安全的,打开了这道大门,从另外一个角度来说又会使生产环境变得危机四伏,如果这个能力被别有用心的人利用,后果将不堪设想。

好也好,坏也罢,不懂,就得学,我尝试者使用JavaAgent,首先做一个简单的尝试,从一个虚拟机 attach 到另外一个虚拟机,并执行指定的代码。


首先我们先创建一个进程1,并让它保持生存,同时打印出进程ID

public class RunningProcess {

    public static void main(String[] args) {
        // TODO Auto-generated method stub

        System.out.println("Current processid = " + getCurrentThreadID());

        while(true) {
            try {
                System.out.println("I'm living...");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }


    }

    /**
     * 获取当前进程ID
     */
    private static Integer getCurrentThreadID()
    {
        RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
        String name = runtime.getName();
        return Integer.parseInt(name.substring(0, name.indexOf("@")));
    }

}

接着我们来处理agent,agent用来做什么? 老规矩先科普一下

JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。
JavaAgent 是运行在 main方法之前的拦截器,JavaAgent有两个拦截状态,一个是premain还有agentmain(注意agentmain是JDK1.6后引入的),他们的执行顺序如下:
premain –> agentmain –> main

废话不多说,先创建一个agent代理处理hook后要执行的代码

public class JavaAgentTest {

     public static void agentmain(String arg) {
            System.out.println("Hello, " + arg);
        }

}

接着在导出agent的jar的时候,记得修改Manifest.MF

Manifest-Version: 1.0
Premain-Class: com.java.agent.demo.JavaAgentTest
Name: JavaAgentTest
Agent-Class: com.java.agent.demo.JavaAgentTest
Can-Retransform-Classes: true
Can-Redefine-Classes: true

然后在eclipse导出的时候,选择修改后的Manifest.MF
这里写图片描述

最后一步了,当然就是启动另外一个进程2,然后attach过去正在执行的进程1,并在进程1上执行agent上的代码。

public class AttachAPI {


    public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {

        String processId = "11456";
        String jarFileName = "J:/JavaAgent.jar";
        VirtualMachine virtualMachine = VirtualMachine.attach(processId);
        try {
            virtualMachine.loadAgent(jarFileName, "World!");
        } finally {
            virtualMachine.detach();
        }
    }

}
  1. 先执行RunningProcess.java
  2. 在console获取processid
  3. 填入AttachAPI.java
  4. 执行AttachAPI.java

Console结果如下,没有比这更简单的了…

Current processid = 5572
I'm living...
I'm living...
I'm living...
Hello, World!
I'm living...
I'm living...

接着我继续做深入一点的试验,替换正在运行的class,于是我把进程1修改了下。

public class RunningProcess {

    public static void main(String[] args) {
        // TODO Auto-generated method stub

        System.out.println("Current processid = " + getCurrentThreadID());

        new DemoPrint().print();

    }

    /**
     * 获取当前进程ID
     */
    private static Integer getCurrentThreadID()
    {
        RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
        String name = runtime.getName();
        return Integer.parseInt(name.substring(0, name.indexOf("@")));
    }

}

创建等待被替换的类DemoPrint.java

package com.nathan.attachAPI.demo;

public class DemoPrint {

    public void print() {
        while(true) {
            try {
                System.out.println("Running.....");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

修改了用来替换class的agent代理jar

public class BugFixAgent {

    public static void agentmain(String arg, Instrumentation inst)
            throws Exception {

         System.out.println("Hello, " + arg);

        Class<?> runningProcess = Class.forName("com.nathan.attachAPI.demo.DemoPrint");

        //Get current path
        System.out.println(ClassLoader.getSystemResource("") );

        File f = new File(arg);
        byte[] reporterClassFile = new byte[(int) f.length()];
        DataInputStream in = new DataInputStream(new FileInputStream(f));
        in.readFully(reporterClassFile);
        in.close();

        // Apply the redefinition
        inst.redefineClasses(new ClassDefinition(runningProcess, reporterClassFile));

        System.out.println("Agent Main Done"); 
    }

}

Manifest.MF

Manifest-Version: 1.0
Premain-Class: com.java.agent.demo.BugFixAgent
Name: BugFixAgent
Agent-Class: com.java.agent.demo.BugFixAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true

修改我们的进程2 AttachAPI.java

public class AttachAPI {


    public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
        // the following strings must be provided by us
        String processId = "11820";
        String jarFileName = "J:/BugFixAgent.jar";
        VirtualMachine virtualMachine = VirtualMachine.attach(processId);
        try {
            virtualMachine.loadAgent(jarFileName, "J:/DemoPrint.class");
        } finally {
            //virtualMachine.detach();
        }
    }

}

最后,把需要修复的类生成字节码,并复制到我本地的J盘(根据你放的地方而定,填在线程2唤醒agent那里),为了简单,修复类在打印那里从 “Running…..” 改成 “Running fix…..”

public class DemoPrint {

    public void print() {
        while(true) {
            try {
                System.out.println("Running fix.....");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
  1. 先执行RunningProcess.java
  2. 在console获取processid
  3. 填入AttachAPI.java
  4. 执行AttachAPI.java

我以为,打印会从 “Running…..” 改成 “Running fix…..”,然鹅….

Current processid = 6320
Running.....
Running.....
Running.....
Running.....
Hello, J:/DemoPrint.class
file:/J:/eclipse-workspace/hotswap-demo/target/classes/
Agent Main Done
Running.....
Running.....

wtf ???为啥还是 “Running…..” !!!!

于是我把DemoPrint改了下…

DemoPrint.java

public class DemoPrint {

    public void print() {
        System.out.println("Running.....");
    }
}

把循环的代码放到线程1

public static void main(String[] args) {
        // TODO Auto-generated method stub

        System.out.println("Current processid = " + getCurrentThreadID());

        while(true) {
            try {
                new DemoPrint().print();

                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }


    }

好,继续试验,按原来的步骤,console终于显示正常的结果了 !!!

Current processid = 2780
Running.....
Running.....
Running.....
Running.....
Hello, J:/DemoPrint.class
file:/J:/eclipse-workspace/hotswap-demo/target/classes/
Agent Main Done
Running fix.....
Running fix.....
Running fix.....

好吧…实验结束。

个人猜想…

为什么第一次替换字节码没有成功?我想很大原因是因为如果改字节码正在被执行,它就无法被替换,而第二次替换的时候,agent应该是在while循环的间隙,即DemoPrint还没被调用的时候进行替换的,如有不对,各位请赐教!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值