Java探针案例

wen java案例 1

本文目录导读:

Java探针案例

  1. Java探针基础概念
  2. 完整实现案例
  3. 实际运行效果
  4. 进阶功能扩展
  5. 注意事项
  6. 实际应用场景

我来为你详细介绍Java探针(Java Agent)的完整案例,包括原理、实现和实际应用。

Java探针基础概念

1 什么是Java探针

Java探针(Java Agent)是JDK 1.5引入的特性,可以在JVM加载类时进行字节码增强,实现方法监控、性能分析、故障诊断等功能。

2 核心原理

  • Premain方式:虚拟机启动时加载,通过java -agentlib参数指定
  • Agentmain方式:虚拟机运行中动态加载,通过Attach机制

完整实现案例

1 项目结构

java-agent-demo
├── pom.xml
├── src
│   └── main
│       └── java
│           ├── com/demo/agent/
│           │   ├── AgentMain.java
│           │   ├── PremainMain.java
│           │   └── transformer/
│           │       └── MethodCostTransformer.java
│           └── com/demo/test/
│               └── TestMain.java

2 Maven配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.demo</groupId>
    <artifactId>java-agent-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- ASM框架用于字节码操作 -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.2</version>
        </dependency>
        <!-- Attach API -->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3 核心探针代码

AgentPremain.java

package com.demo.agent;
import com.demo.agent.transformer.MethodCostTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.List;
/**
 * Java探针启动入口(premain方式)
 */
public class AgentPremain {
    /**
     * JVM启动时加载agent,会调用此方法
     * 
     * @param agentArgs agent参数
     * @param inst Instrumentation实例
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("========== Java Agent启动(premain) ==========");
        System.out.println("Agent参数: " + agentArgs);
        System.out.println("JVM启动参数: " + getJvmArgs());
        // 添加字节码转换器
        inst.addTransformer(new MethodCostTransformer(), true);
        System.out.println("Agent初始化完成,开始监控方法耗时...");
    }
    /**
     * 获取当前Java进程的主要信息
     */
    private static String getJvmArgs() {
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        List<String> inputArgs = runtimeMXBean.getInputArguments();
        return String.join(" ", inputArgs);
    }
}

AgentMain.java(动态Attach方式)

package com.demo.agent;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.lang.instrument.Instrumentation;
import java.util.List;
/**
 * Java探针(agentmain方式 - 动态加载)
 */
public class AgentMain {
    /**
     * 动态加载agent时会调用此方法
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("========== Java Agent动态加载(agentmain) ==========");
        System.out.println("Agent参数: " + agentArgs);
        // 添加字节码转换器
        inst.addTransformer(new MethodCostTransformer(), true);
        System.out.println("Agent动态加载完成");
    }
    /**
     * 主动加载到指定进程的工具方法
     */
    public static void attachToProcess(String pid, String agentPath) {
        try {
            VirtualMachine vm = VirtualMachine.attach(pid);
            vm.loadAgent(agentPath, "start");
            vm.detach();
            System.out.println("成功将Agent加载到进程: " + pid);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 列出当前所有Java进程
     */
    public static void listJavaProcesses() {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        System.out.println("当前Java进程列表:");
        for (VirtualMachineDescriptor vmd : list) {
            System.out.println("  PID: " + vmd.id() + ", 名称: " + vmd.displayName());
        }
    }
}

MethodCostTransformer.java

package com.demo.agent.transformer;
import org.objectweb.asm.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
/**
 * 方法耗时统计的字节码转换器
 * 使用ASM框架进行字节码增强
 */
public class MethodCostTransformer implements ClassFileTransformer {
    // 需要监控的包名前缀
    private static final String MONITOR_PACKAGE = "com/demo/test/";
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                           ProtectionDomain protectionDomain, byte[] classfileBuffer) 
            throws IllegalClassFormatException {
        // 只监控指定包下的类
        if (className == null || !className.startsWith(MONITOR_PACKAGE)) {
            return null;
        }
        try {
            System.out.println("开始增强类: " + className.replace('/', '.'));
            // 使用ASM读取原始字节码
            ClassReader classReader = new ClassReader(classfileBuffer);
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
            // 创建访问器
            ClassVisitor classVisitor = new MethodCostClassVisitor(classWriter, className);
            // 执行转换
            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
            System.out.println("增强完成: " + className.replace('/', '.'));
            return classWriter.toByteArray();
        } catch (Exception e) {
            System.err.println("增强类失败: " + className + ", 错误: " + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 类访问器
     */
    private static class MethodCostClassVisitor extends ClassVisitor {
        private final String className;
        public MethodCostClassVisitor(ClassVisitor cv, String className) {
            super(Opcodes.ASM9, cv);
            this.className = className;
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, 
                                        String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
            // 排除构造方法和静态初始化块
            if (!name.equals("<init>") && !name.equals("<clinit>")) {
                return new MethodCostVisitor(mv, className, name, descriptor);
            }
            return mv;
        }
    }
    /**
     * 方法访问器 - 核心增强逻辑
     */
    private static class MethodCostVisitor extends MethodVisitor {
        private final String className;
        private final String methodName;
        private final String methodDesc;
        // 用于存储时间戳的变量索引
        private int timestampLocal;
        // 是否已增加局部变量
        private boolean localAdded = false;
        public MethodCostVisitor(MethodVisitor mv, String className, String methodName, String methodDesc) {
            super(Opcodes.ASM9, mv);
            this.className = className;
            this.methodName = methodName;
            this.methodDesc = methodDesc;
        }
        @Override
        public void visitCode() {
            super.visitCode();
            if (!localAdded) {
                // 在局部变量表中增加一个long类型的变量用于存储时间戳
                timestampLocal = 0; // 需要根据实际情况获取
                localAdded = true;
            }
            // 方法入口处插入代码
            // long startTime = System.currentTimeMillis();
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", 
                              "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LSTORE, timestampLocal);
            System.out.println("  已添加方法入口监控: " + methodName);
        }
        @Override
        public void visitInsn(int opcode) {
            // 在方法返回指令前插入时间计算代码
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                // long endTime = System.currentTimeMillis();
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",
                                  "currentTimeMillis", "()J", false);
                mv.visitVarInsn(Opcodes.LSTORE, timestampLocal + 2);
                // long cost = endTime - startTime
                mv.visitVarInsn(Opcodes.LLOAD, timestampLocal + 2);
                mv.visitVarInsn(Opcodes.LLOAD, timestampLocal);
                mv.visitInsn(Opcodes.LSUB);
                mv.visitVarInsn(Opcodes.LSTORE, timestampLocal + 4);
                // System.out.println("方法名: " + cost + "ms")
                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", 
                                 "Ljava/io/PrintStream;");
                mv.visitLdcInsn(className.replace('/', '.') + "." + methodName + " 耗时: ");
                mv.visitVarInsn(Opcodes.LLOAD, timestampLocal + 4);
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/String",
                                  "valueOf", "(J)Ljava/lang/String;", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder",
                                  "toString", "()Ljava/lang/String;", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                                  "println", "(Ljava/lang/String;)V", false);
                System.out.println("  已添加方法出口监控: " + methodName);
            }
            super.visitInsn(opcode);
        }
    }
}

4 测试代码

TestMain.java

package com.demo.test;
import com.demo.agent.AgentMain;
import java.util.concurrent.TimeUnit;
/**
 * 测试Java探针的目标类
 */
public class TestMain {
    public static void main(String[] args) throws Exception {
        System.out.println("===== 启动测试程序 =====");
        TestService service = new TestService();
        for (int i = 0; i < 10; i++) {
            service.processOrder("订单-" + i);
            Thread.sleep(1000);
        }
        System.out.println("===== 测试程序结束 =====");
    }
}
class TestService {
    public void processOrder(String orderId) {
        System.out.println("处理订单: " + orderId);
        // 模拟业务逻辑
        validateOrder(orderId);
        updateInventory(orderId);
        notifyUser(orderId);
    }
    private void validateOrder(String orderId) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    private void updateInventory(String orderId) {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    private void notifyUser(String orderId) {
        try {
            Thread.sleep(150);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

5 MANIFEST.MF配置

创建 src/main/resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: com.demo.agent.AgentPremain
Agent-Class: com.demo.agent.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Permit-Redefinition-Classes: true

6 使用说明

启动时加载(premain)

# 打包
mvn clean package
# 运行测试程序(不加载Agent)
java -cp target/java-agent-demo-1.0-SNAPSHOT.jar com.demo.test.TestMain
# 运行测试程序(加载Agent)
java -javaagent:target/java-agent-demo-1.0-SNAPSHOT.jar -cp target/java-agent-demo-1.0-SNAPSHOT.jar com.demo.test.TestMain

动态加载(agentmain)

创建一个动态加载的触发类:

package com.demo.test;
import com.demo.agent.AgentMain;
public class AttachTest {
    public static void main(String[] args) {
        // 列出所有Java进程
        AgentMain.listJavaProcesses();
        // 指定目标进程PID
        String targetPid = "12345";
        String agentPath = "/path/to/agent.jar";
        // 动态加载
        AgentMain.attachToProcess(targetPid, agentPath);
    }
}

实际运行效果

加载Agent后,控制台输出:

========== Java Agent启动(premain) ==========
Agent参数: null
JVM启动参数: -javaagent:target/java-agent-demo-1.0-SNAPSHOT.jar
Agent初始化完成,开始监控方法耗时...
开始增强类: com.demo.test.TestMain
增强完成: com.demo.test.TestMain
开始增强类: com.demo.test.TestService
增强完成: com.demo.test.TestService
===== 启动测试程序 =====
处理订单: 订单-0
com.demo.test.TestService.validateOrder 耗时: 102ms
com.demo.test.TestService.updateInventory 耗时: 201ms
com.demo.test.TestService.notifyUser 耗时: 153ms
com.demo.test.TestService.processOrder 耗时: 461ms
...

进阶功能扩展

1 加入耗时统计存储

public class MethodCostCollector {
    private static final Map<String, List<Long>> METHOD_COSTS = new ConcurrentHashMap<>();
    public static void recordMethodCost(String methodName, long cost) {
        METHOD_COSTS.computeIfAbsent(methodName, k -> new ArrayList<>()).add(cost);
    }
    public static void printStats() {
        METHOD_COSTS.forEach((method, costs) -> {
            DoubleSummaryStatistics stats = costs.stream()
                .mapToDouble(Long::doubleValue)
                .summaryStatistics();
            System.out.println(String.format(
                "%s: 次数=%d, 平均=%.2fms, 最大=%.2fms, 最小=%.2fms",
                method, stats.getCount(), stats.getAverage(), 
                stats.getMax(), stats.getMin()
            ));
        });
    }
}

2 支持参数配置

public class AgentConfig {
    private static Properties properties = new Properties();
    public static void load(String configPath) {
        try (InputStream is = new FileInputStream(configPath)) {
            properties.load(is);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static boolean shouldMonitor(String className) {
        String monitorPackage = properties.getProperty("monitor.package");
        return className != null && className.startsWith(monitorPackage);
    }
    public static boolean isEnableMonitor(String methodName) {
        String excludeMethods = properties.getProperty("exclude.methods", "");
        return !Arrays.asList(excludeMethods.split(",")).contains(methodName);
    }
}

注意事项

  1. 性能开销:字节码增强会带来一定性能影响,建议只监控关键方法
  2. 兼容性:注意ASM版本与Java版本兼容性
  3. 异常处理:增强代码需要完善的异常处理,避免影响业务逻辑
  4. 类加载问题:注意处理被多个ClassLoader加载的类
  5. 安全考虑:生产环境使用需经过严格测试

实际应用场景

  • APM监控:统计方法耗时、请求链路追踪
  • 性能分析:定位慢方法、内存泄漏诊断
  • 日志增强:自动记录方法入参、返回值
  • 热部署:动态修改类行为而不重启应用
  • 安全审计:记录敏感操作调用信息

这个案例展示了Java探针的完整实现,你可以根据实际需求进行扩展和修改。

抱歉,评论功能暂时关闭!