如何使用Java Agent?

  • Post category:Java

使用Java Agent可以在运行时修改字节码、探求程序执行、修改和增强程序行为。下面详细介绍如何使用Java Agent。

1. 编写代理程序

Java agent程序需要提供一个代理类,它实现了 java.lang.instrument.ClassFileTransformer 接口的 transform 方法,用于修改某些字节码。下面是一个示例代码:

package com.example.agent;

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

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.equals("com/example/MyClass")) {
            System.out.println("Modifying " + className);
            return modifyClass(classfileBuffer);
        }
        return classfileBuffer;
    }

    private byte[] modifyClass(byte[] classfileBuffer) {
        // modify the class byte array
        ...
        return modifiedByteArray;
    }
}

在上面的代码中,transform 方法中,如果发现类名是 com.example.MyClass,则调用 modifyClass 方法修改该类的字节码。修改后的字节码即为 modifiedByteArray

2. 打包代理程序

将代理类打包为jar,需要在 META-INF/MANIFEST.MF 文件中声明一个 Agent-Class 属性指定代理类名,如下所示:

Manifest-Version: 1.0
Premain-Class: com.example.agent.Agent
Agent-Class: com.example.agent.MyTransformer

上面的示例中,Agent-Class 属性值为 com.example.agent.MyTransformer,即代理类的完整包名。

3. 使用Java Agent

有两种方式使用Java Agent:在程序启动前使用 java -javaagent 命令行参数,或在程序内部使用 Instrumentation API。下面分别介绍这两种方式。

方式一:启动参数方式

public static void main(String[] args) {
    try {
        java.lang.instrument.Instrumentation inst = java.lang.instrument.Instrumentation();
        inst.addTransformer(new MyTransformer());
    } catch (Exception e) {
        e.printStackTrace();
    }
    ...
}

上面的代码中,在程序启动时通过 java.lang.instrument.Instrumentation API 来添加代理程序 MyTransform

修改后的程序启动命令如下:

java -javaagent:/path/to/my-agent.jar MyMainClass

方式二:Instrumentation API 方式

public class MyClass {
    public static void main(String[] args) {
        Instrumentation inst = getInstrumentation();
        inst.addTransformer(new MyTransformer());
        inst.retransformClasses(MyClass.class);
        ...
    }
    private static Instrumentation getInstrumentation() {
        try {
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            Class clazz = Class.forName("java.lang.instrument.Instrumentation", false, cl);
            Method method = clazz.getMethod("getInstrumentation", new Class[0]);
            return (Instrumentation)method.invoke(null, new Object[0]);
        } catch (Exception e) {
            throw new RuntimeException("Cannot load instrumentation", e);
        }
    }
}

上面的代码中,在程序运行时通过 Instrumentation API 来添加代理程序 MyTransform

示例

下面提供一个示例:如何通过Java Agent监控方法执行时间:

package com.example.agent;

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

public class MethodTimerAgent {
    static Instrumentation inst;

    public static void premain(String agentArgs, Instrumentation inst) {
        MethodTimerAgent.inst = inst;
        inst.addTransformer(new MethodTimerTransformer());
    }

    static class MethodTimerTransformer implements ClassFileTransformer {
        public byte[] transform(ClassLoader loader,
                                String className,
                                Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain,
                                byte[] classfileBuffer) throws IllegalClassFormatException {
            if (!className.startsWith("java/") &&
                    !className.startsWith("sun/") &&
                    !className.startsWith("javax/")) {
                System.out.println("Transforming class " + className);
                return addTiming(classfileBuffer);
            }
            return null;
        }

        private byte[] addTiming(byte[] classfileBuffer) {
            // todo: add timing logic
            return classfileBuffer;
        }
    }

    public static void redefineClasses(Class<?>... classes) {
        inst.retransformClasses(classes);
    }

    public static void main(String[] args) throws InterruptedException {
        MyClass myObject = new MyClass();
        System.out.println(myObject.sum(1, 2));
        MethodTimerAgent.redefineClasses(MyClass.class);

        System.out.println(myObject.sum(3, 4));
        Thread.sleep(10000); // wait for a while for the background thread to print the timing statistics
    }
}

class MyClass {
    public int sum(int a, int b) {
        return a + b;
    }
}

在上面的代码中,MethodTimerAgent 类是代理程序,它会通过在 premain 方法中调用 inst.addTransformer 方法来添加转换器 MethodTimerTransformerMethodTimerTransformer 会在类加载时指定的类中插入计时逻辑。

运行上面的程序后,会输出 Transforming class com/example/agent/MethodTimerAgentTransforming class com/example/agent/MyClass。每次调用 MyClass.sum 方法时,都会显示该方法的耗时。通过重新定义 MyClass 类,可以添加或删除计时逻辑。