浅谈JavaAgent
字数 1626 2025-08-24 07:48:33

JavaAgent 技术详解

一、JavaAgent 概述

JavaAgent 是 Java 提供的一种特殊机制,允许开发者在 JVM 启动时或运行时动态修改字节码。JavaAgent 主要通过 -javaagent 参数指定一个 jar 包来实现,该 jar 包需要满足以下要求:

  1. MANIFEST.MF 文件中必须指定 Premain-Class 或 Agent-Class 项
  2. 指定的类必须实现 premain() 或 agentmain() 方法

JavaAgent 主要分为两种加载方式:

  • 启动时加载(Premain)
  • 运行时加载(Agentmain)

二、启动时加载 Agent (Premain)

2.1 基本概念

Premain-Class 指定的类会在 main 函数之前执行,函数声明有两种形式:

public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)

其中 Instrumentation inst 参数的方法优先级更高。

2.2 实现示例

  1. 创建 Premain 类:
import java.lang.instrument.Instrumentation;

public class PreMainDemo {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println(agentArgs);
        for (int i = 0; i < 5; i++) {
            System.out.println("premain is loading.....");
        }
    }
}
  1. 创建 MANIFEST.MF 文件:
Manifest-Version: 1.0
Premain-Class: Agent.PreMainDemo
  1. 打包:
jar cvfm agent.jar agent.mf Agent/PreMainDemo.class
  1. 测试类:
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello,Sentiment!");
    }
}
  1. 运行:
java -javaagent:agent.jar=Sentiment -jar hello.jar

2.3 局限性

  • 必须在目标程序启动前加载
  • 无法对已运行的 JVM 进程生效

三、运行时加载 Agent (Agentmain)

3.1 基本概念

Agentmain 通过 Attach API 实现运行时加载,解决了 Premain 的局限性。主要函数声明:

public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)

3.2 Attach API 核心类

  1. VirtualMachine:

    • list(): 获取所有 JVM 列表
    • attach(String id): 根据 pid 连接到 JVM
    • detach(): 断开连接
    • loadAgent(String agent): 加载 agent
  2. VirtualMachineDescriptor: 描述虚拟机的容器类

3.3 实现示例

  1. Agent 类:
public class AgentMainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain start.");
    }
}
  1. MANIFEST.MF:
Manifest-Version: 1.0
Agent-Class: Agent.AgentMainDemo
  1. 目标程序:
public class Hello {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Hello.main() in test project start!!");
        Thread.sleep(300000000);
        System.out.println("Hello.main() in test project end!!");
    }
}
  1. Attach 程序:
public class AttchDemo {
    public static void main(String[] args) throws AgentLoadException, IOException, 
            AgentInitializationException, AttachNotSupportedException {
        VirtualMachine attach = VirtualMachine.attach("14460"); // 目标进程ID
        attach.loadAgent("D:\\java\\AgentMemory\\target\\classes\\agent.jar");
        attach.detach();
    }
}

四、Instrumentation 接口详解

Instrumentation 是与 JVM 交互的核心接口,主要方法:

  1. addTransformer(ClassFileTransformer transformer): 添加类转换器
  2. removeTransformer(ClassFileTransformer transformer): 移除类转换器
  3. retransformClasses(Class<?>... classes): 重新转换类
  4. isModifiableClass(Class<?> theClass): 判断类是否可修改
  5. getAllLoadedClasses(): 获取所有已加载类

4.1 获取已加载类示例

public class AgentMainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        Class[] classes = inst.getAllLoadedClasses();
        for (Class aClass : classes) {
            String result = "class ==> " + aClass.getName();
            System.out.println(result);
        }
    }
}

4.2 检查类可修改性

public class AgentMainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        Class[] classes = inst.getAllLoadedClasses();
        for (Class aClass : classes) {
            String result = "class ==> " + aClass.getName() + "\n\t" + 
                    "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
            if (result.contains("true")) {
                System.out.println(result);
            }
        }
    }
}

五、动态修改字节码

5.1 ClassFileTransformer 接口

public interface ClassFileTransformer {
    default byte[] transform(ClassLoader loader, String className, 
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
            byte[] classfileBuffer) {
        ....
    }
}

5.2 结合 Javassist 修改字节码

  1. 目标类:
public class Hello {
    public void Hello() {
        System.out.println("This is Sentiment !");
    }
}
  1. 测试程序:
public class HelloWorld {
    public static void main(String[] args) throws InterruptedException {
        Hello h1 = new Hello();
        h1.Hello();
        Thread.sleep(15000);
        Hello h2 = new Hello();
        h2.Hello();
    }
}
  1. AgentMain 类:
public class AgentMainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) 
            throws UnmodifiableClassException {
        Class[] classes = inst.getAllLoadedClasses();
        for (Class aClass : classes) {
            if (aClass.getName().equals(TransformerDemo.editClassName)) {
                inst.addTransformer(new TransformerDemo(), true);
                inst.retransformClasses(aClass);
            }
        }
    }
}
  1. Transformer 实现:
public class TransformerDemo implements ClassFileTransformer {
    public static final String editClassName = "Agent.Hello";
    public static final String editMethod = "Hello";

    @Override
    public byte[] transform(ClassLoader loader, String className, 
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
            byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            ClassPool cp = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(ccp);
            }
            CtClass ctc = cp.get(editClassName);
            CtMethod method = ctc.getDeclaredMethod(editMethod);
            // 修改方法体
            String source = "{System.out.println(\"Has been modified\");}";
            method.setBody(source);
            byte[] bytes = ctc.toBytecode();
            ctc.detach();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
  1. MANIFEST.MF 配置:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: Agent.AgentMainDemo

六、注意事项

  1. 对于 Web 服务器环境,需要使用 insertClassPath() 添加类搜索路径:
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
  1. 修改已加载类需要在 MANIFEST.MF 中添加:

    • Can-Redefine-Classes: true
    • Can-Retransform-Classes: true
  2. 注意 MANIFEST.MF 文件格式要求,最后必须有空行

  3. 使用 Attach API 需要引入 tools.jar(通常位于 JDK 的 lib 目录下)

七、总结

JavaAgent 技术提供了强大的字节码操作能力,可以用于:

  • 性能监控
  • 热部署
  • AOP 实现
  • 安全防护
  • 调试工具开发

掌握 JavaAgent 技术需要理解 JVM 类加载机制、字节码操作工具(如 Javassist、ASM)以及 Instrumentation API 的使用。

JavaAgent 技术详解 一、JavaAgent 概述 JavaAgent 是 Java 提供的一种特殊机制,允许开发者在 JVM 启动时或运行时动态修改字节码。JavaAgent 主要通过 -javaagent 参数指定一个 jar 包来实现,该 jar 包需要满足以下要求: MANIFEST.MF 文件中必须指定 Premain-Class 或 Agent-Class 项 指定的类必须实现 premain() 或 agentmain() 方法 JavaAgent 主要分为两种加载方式: 启动时加载(Premain) 运行时加载(Agentmain) 二、启动时加载 Agent (Premain) 2.1 基本概念 Premain-Class 指定的类会在 main 函数之前执行,函数声明有两种形式: 其中 Instrumentation inst 参数的方法优先级更高。 2.2 实现示例 创建 Premain 类: 创建 MANIFEST.MF 文件: 打包: 测试类: 运行: 2.3 局限性 必须在目标程序启动前加载 无法对已运行的 JVM 进程生效 三、运行时加载 Agent (Agentmain) 3.1 基本概念 Agentmain 通过 Attach API 实现运行时加载,解决了 Premain 的局限性。主要函数声明: 3.2 Attach API 核心类 VirtualMachine : list() : 获取所有 JVM 列表 attach(String id) : 根据 pid 连接到 JVM detach() : 断开连接 loadAgent(String agent) : 加载 agent VirtualMachineDescriptor : 描述虚拟机的容器类 3.3 实现示例 Agent 类: MANIFEST.MF: 目标程序: Attach 程序: 四、Instrumentation 接口详解 Instrumentation 是与 JVM 交互的核心接口,主要方法: addTransformer(ClassFileTransformer transformer) : 添加类转换器 removeTransformer(ClassFileTransformer transformer) : 移除类转换器 retransformClasses(Class<?>... classes) : 重新转换类 isModifiableClass(Class<?> theClass) : 判断类是否可修改 getAllLoadedClasses() : 获取所有已加载类 4.1 获取已加载类示例 4.2 检查类可修改性 五、动态修改字节码 5.1 ClassFileTransformer 接口 5.2 结合 Javassist 修改字节码 目标类: 测试程序: AgentMain 类: Transformer 实现: MANIFEST.MF 配置: 六、注意事项 对于 Web 服务器环境,需要使用 insertClassPath() 添加类搜索路径: 修改已加载类需要在 MANIFEST.MF 中添加: Can-Redefine-Classes: true Can-Retransform-Classes: true 注意 MANIFEST.MF 文件格式要求,最后必须有空行 使用 Attach API 需要引入 tools.jar(通常位于 JDK 的 lib 目录下) 七、总结 JavaAgent 技术提供了强大的字节码操作能力,可以用于: 性能监控 热部署 AOP 实现 安全防护 调试工具开发 掌握 JavaAgent 技术需要理解 JVM 类加载机制、字节码操作工具(如 Javassist、ASM)以及 Instrumentation API 的使用。