http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

概述

在 Java SE 5 当中,开发者只能在 premain 当中施展想象力,所作的 Instrumentation 也仅限与 main 函数执行前,这样的方式存在一定的局限性。 在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。 在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。 跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类: public static void agentmain (String agentArgs, Instrumentation inst); [1] public static void agentmain (String agentArgs); [2] 同样,[1] 的优先级比 [2] 高,将会被优先执行。

与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。 可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢? 在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API。 Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。 Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

实例

业务类:

package agentmain;

public class TransClass {
    public int getNumber() {
        return 0;
    }
}

使用业务类:

package agentmain;

public class TestMainInJar {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(new TransClass().getNumber());
        while (true) {
            Thread.sleep(2000);
            int number = new TransClass().getNumber();
            System.out.println(number);
        }
    }
}

PS:如果业务类发生变化,使用者打印会感知到。

下面是 instrument 相关的类: 动态Agent:

package agentmain;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException,
            InterruptedException {
        System.out.println("Agent Main Done");
        inst.addTransformer(new Transformer(), true);
        inst.retransformClasses(TransClass.class);
    }
}

业务转换类:

package agentmain;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Transformer implements ClassFileTransformer {

    public static final String classNumberReturns2 = "/home/conquer/Desktop/aaaa/TransClass.class";

    public static byte[] getBytesFromFile(String fileName) {
        try {
            System.out.println(System.getProperty("user.home"));
            // precondition
            File file = new File(fileName);
            InputStream is = new FileInputStream(file);
            long length = file.length();
            byte[] bytes = new byte[(int) length];

            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while (offset < bytes.length
                    && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("Could not completely read file "
                        + file.getName());
            }
            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            return null;
        }
    }

    public byte[] transform(ClassLoader l, String className, Class<?> c,
                            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        System.out.println(className);
        if (!className.contains("TransClass")) {
            System.out.println("no");
            return null;
        } else {
            System.out.println("yes");
        }
        return getBytesFromFile(classNumberReturns2);

    }
}

业务转换类会从 /home/conquer/Desktop/aaaa/TransClass.class 这个位置加载一个新的类以替换原有的业务类,注意这个类必需和原有业务类全名称相同,测试过程可以将返回值改成2或其它,以区分发生了变化,让业务使用类打印出一个新值。 将上面两个类达成jar包,如 agentmain.jar,并添加:META-INF/MANIFEST.MF,内容:

Manifest-Version: 1.0
Agent-Class: agentmain.AgentMain

好了,开始测试,先运行TestMainInJar循环打印当前的业务方法返回值,然后运行测试类:

package agentmain;

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

import java.util.List;

public class AttachTest extends Thread {

    private final List<VirtualMachineDescriptor> listBefore;

    private final String jar;

    AttachTest(String attachJar, List<VirtualMachineDescriptor> vms) {
        listBefore = vms;  // 记录程序启动时的 VM 集合
        jar = attachJar;
    }

    public void run() {
        VirtualMachine vm = null;
        List<VirtualMachineDescriptor> listAfter = null;
        try {
            int count = 0;
            while (true) {
                listAfter = VirtualMachine.list();
                for (VirtualMachineDescriptor vmd : listAfter) {
                    if (!listBefore.contains(vmd)) {
                        // 如果 VM 有增加,我们就认为是被监控的 VM 启动了
                        // 这时,我们开始监控这个 VM
                        vm = VirtualMachine.attach(vmd);
                        break;
                    }
                }
                Thread.sleep(500);
                count++;
                if (null != vm || count >= 10) {
                    break;
                }
            }
            vm.loadAgent(jar);
//            vm.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
//        new AttachThread("TestInstrument1.jar", VirtualMachine.list()).run();

//        VirtualMachine attach = VirtualMachine.attach("5741");
//        System.out.println(attach.id());
        for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
            System.out.println(vmd);
            if (vmd.displayName().contains("TestMainInJar")) {
                VirtualMachine vm = VirtualMachine.attach(vmd);
                vm.loadAgent("/home/conquer/Desktop/aaaa/agentmain.jar");
                System.out.println("loaded");
                vm.detach();
                System.out.println("detached");
                break;
            }
        }
    }
}

这样就能看到打印结果在运行agent之后由0变成2,类字节码被成功替换了。

最后说明1

测试使用用例的时候我们是通过每次new一个新的对象来观察修改结果,其实不是必需new的,因为方法的执行序列是存储在方法区(class文件的定义区),无论使用新的实例还是旧的(用旧的class创建的)实例都是可以感知到class的变化的,如,测试用例可以修改为:

package agentmain;

public class TestMainInJar {
    static TransClass transClass = new TransClass();
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            Thread.sleep(2000);
            System.out.println(transClass + " ==: " + transClass.getNumber());
        }
    }
}

以上,我们将TransClass定义为一个全局变量,循环调用打印一个旧实例的方法返回值,同样可以看到class替换后的结果。(PS:如果对jvm内部理解透彻的话是很好理解的,实例在内存中只是保存了区别于其它实例的属性或成员变量,其方法的执行序列依然走的是class的定义)

最后说明2

相对于jdk5只能通过启动脚本添加javaagent的方式植入代理,jdk6的动态attach也只是免去了修改启动脚本和不用重启的工作,并没有添加其它新的特性,即使不使用动态attach而是使用脚本添加javaagent的方式也可以达到随时修改class定义的目的,无论通过什么方式我们只要获取了 Instrumentation 实例,然后调用其addTransformer方法添加类转换器再调用retransformClasses就可以转换一个类的字节序列了,这里需要注意的是retransformClasses是jdk1.6定义的,jdk1.5只能使用redefineClasses,retransformClasses功能强大使用简单,但有不能修改方法签名,只能修改body等约束,redefineClasses则是一个可定细节制化的选择。

最后说明3

大名鼎鼎的Btrace就是基于jdk6实现的,它使用到了jdk6的动态attach(非脚本模式),同时使用到了jdk6提供的retransformClasses,其实要方便地操作字节码最好还是基于JDK6做吧。