Java 5.0的Instrumentation特性

Java 5发布有一段时间了,Instrumentation这个feature是Java 5新提供的。其方式是通过修改字节码的方式使得Java开发人员能够操作类。官方文档说主要是给工具提供修改应用的状态、行为使用的:)
先来个简单的例子看看到底什么是Instrumentation:
在JSE 1.5.0的Javadoc的看到java.lang.instrument仅有两个接口ClassFileTransformer和Instrumentation。我们就看看着两个接口的用法:

public class Greeting implements ClassFileTransformer {

    //字节码转换在这个方法中进行。
    public byte[] transform(ClassLoader arg0, String classname, Class arg2, ProtectionDomain arg3, byte[] arg4)
    throws IllegalClassFormatException {
        System.out.printf("hello:" + classname);
        return new byte[]{};
    }

    //options是通过命令行传递给虚拟机的参数。
    public static void premain(String options, Instrumentation ins) {
        if (options != null) {
            System.out.printf(" I've been called with options: \"%s\"\n", options);
        } else
            System.out.println(" I've been called with no options.");
            ins.addTransformer(new Greeting());
        }

    }

    public class Sample {

    /**
    * @param args
    */
    public static void main(String[] args) {
        (new Sample()).hello();

    }

    public void hello() {
        for (int i = 0; i < 10000; i++) {
        int index =0;
        index++;
        }
    }

}

使用命令行参数的命令行:

java -javaagent:Greeting.jar="Hello, Sample" Sample

因此下一步是需要打个jar包,jar包中包含META-INF/MANIFEST.MF和Class文件。
其中META-INF/MANIFEST.MF的内容如下:
Manifest-Version: 1.0
Premain-Class: Timing

包含的类文件有:Greeting.class和Sample.class
打包:

jar cvfM greeting.jar *

输出:
adding: Greeting.class(in = 1774) (out= 869)(deflated 51%)
adding: META-INF/(in = 0) (out= 0)(stored 0%)
adding: META-INF/MANIFEST.MF(in = 44) (out= 46)(deflated -4%)
adding: Sample.class(in = 556) (out= 371)(deflated 33%)

运行命令行:

java -javaagent:greeting.jar="Hello,Sample" Greeting

控制台输出:
I’ve been called with options: “Hello,Sample”

运行命令行:

java -javaagent:greeting.jar="Hello,Sample" Sample

控制台输出:
I’ve been called with options: “Hello,Sample”
hello:Sample

通过这个例子估计Instrutment API使用的方法已经基本上有了个理解了。
下面就是举一个用apache bcel构造bytecode的Instrutment的实际的例子:

使用 instrumentation ,使用Apache 开源项目 BCEL修改bytecode,实现用于计算一个方法运行时间的功能。这种方式,用于性能测量的语句与业务逻辑完全分离,同时也可以用于测量任意类的任意方法的 运行时间,提高了代码的重用性。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.InstructionConstants;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.PUSH;
import org.apache.bcel.generic.Type;

public class Timing implements ClassFileTransformer {

    private String methodName;

    private Timing(String methodName) {
        this.methodName = methodName;
        System.out.println(methodName);
    }

    public byte[] transform(ClassLoader loader, String className, Class cBR, java.security.ProtectionDomain pD, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            ClassParser cp = new ClassParser(new java.io.ByteArrayInputStream(classfileBuffer), className + ".java");
            JavaClass jclas = cp.parse();
            ClassGen cgen = new ClassGen(jclas);
            Method[] methods = jclas.getMethods();
            int index;
            for (index = 0; index < methods.length; index++) {
                if (methods[index].getName().equals(methodName)) {
                    break;
                }
            }

            if (index < methods.length) {
                addTimer(cgen, methods[index]);
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                cgen.getJavaClass().dump(bos);
                return bos.toByteArray();
            }
            System.err.println("Method " + methodName + " not found in " + className);
            System.exit(0);

        } catch (IOException e) {
            System.err.println(e);
            System.exit(0);
        }
        return null; // No transformation required
    }

    private static void addTimer(ClassGen cgen, Method method) {

        // set up the construction tools
        InstructionFactory ifact = new InstructionFactory(cgen);
        InstructionList ilist = new InstructionList();
        ConstantPoolGen pgen = cgen.getConstantPool();
        String cname = cgen.getClassName();
        MethodGen wrapgen = new MethodGen(method, cname, pgen);
        wrapgen.setInstructionList(ilist);

        // rename a copy of the original method
        MethodGen methgen = new MethodGen(method, cname, pgen);
        cgen.removeMethod(method);
        String iname = methgen.getName() + "_timing";
        methgen.setName(iname);
        cgen.addMethod(methgen.getMethod());
        Type result = methgen.getReturnType();

        // compute the size of the calling parameters
        Type[] parameters = methgen.getArgumentTypes();
        int stackIndex = methgen.isStatic() ? 0 : 1;
        for (int i = 0; i < parameters.length; i++) {
            stackIndex += parameters[i].getSize();
        }

        // save time prior to invocation
        ilist.append(ifact.createInvoke("java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
        ilist.append(InstructionFactory.createStore(Type.LONG, stackIndex));

        // call the wrapped method
        int offset = 0;
        short invoke = Constants.INVOKESTATIC;
        if (!methgen.isStatic()) {
            ilist.append(InstructionFactory.createLoad(Type.OBJECT, 0));
            offset = 1;
            invoke = Constants.INVOKEVIRTUAL;
        }
        for (int i = 0; i < parameters.length; i++) {
            Type type = parameters[i];
            ilist.append(InstructionFactory.createLoad(type, offset));
            offset += type.getSize();
        }
        ilist.append(ifact.createInvoke(cname, iname, result, parameters, invoke));

        // store result for return later
        if (result != Type.VOID) {
            ilist .append(InstructionFactory.createStore(result, stackIndex + 2));
        }

        // print time required for method call
        ilist.append(ifact.createFieldAccess("java.lang.System", "out", new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
        ilist.append(InstructionConstants.DUP);
        ilist.append(InstructionConstants.DUP);
        String text = "Call to method " + methgen.getName() + " took ";
        ilist.append(new PUSH(pgen, text));
        ilist.append(ifact.createInvoke("java.io.PrintStream", "print", Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));
        ilist.append(ifact.createInvoke("java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
        ilist.append(InstructionFactory.createLoad(Type.LONG, stackIndex));
        ilist.append(InstructionConstants.LSUB);
        ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
        Type.VOID, new Type[] { Type.LONG }, Constants.INVOKEVIRTUAL));
        ilist.append(new PUSH(pgen, " ms."));
        ilist.append(ifact.createInvoke("java.io.PrintStream", "println", Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));

        // return result from wrapped method call
        if (result != Type.VOID) {
            ilist.append(InstructionFactory.createLoad(result, stackIndex + 2));
        }
        ilist.append(InstructionFactory.createReturn(result));

        // finalize the constructed method
        wrapgen.stripAttributes(true);
        wrapgen.setMaxStack();
        wrapgen.setMaxLocals();
        cgen.addMethod(wrapgen.getMethod());
        ilist.dispose();
    }

    public static void premain(String options, Instrumentation ins) {
        if (options != null) {
            ins.addTransformer(new Timing(options));
        } else {
            System.out.println("Usage: java -javaagent:Timing.jar=\"class:method\"");
            System.exit(0);
        }

    }
}

打jar包:

$ jar cvfM timing.jar *

输出:
adding: META-INF/(in = 0) (out= 0)(stored 0%)
adding: META-INF/MANIFEST.MF(in = 44) (out= 46)(deflated -4%)
adding: Sample.class(in = 556) (out= 371)(deflated 33%)
adding: Timing.class(in = 7372) (out= 3442)(deflated 53%)

运行命令行:

$ java -classpath bcel-5.2.jar -javaagent:timing.jar="hello" Sample

输出:
hello
Call to method hello_timing took 2047 ms.

运行命令:

$ java -classpath bcel-5.2.jar -javaagent:timing.jar="main" Sample

输出:
main
Call to method main_timing took 2469 ms.

通过这段代码,基本能够了解Instrument的用处之一了:)

参考:http://www.ibm.com/developerworks/cn/java/j-lo-instrumentation/