• Java Agent-可以让你在不重启应用的情况下对运行中的JVM进行字节码增强
  • 发布于 2天前
  • 68 热度
    0 评论
在生产环境中,线上问题定位往往面临着巨大挑战 —— 我们不能随意重启应用,更不能暂停服务进行调试。Java Agent技术为我们提供了一种优雅的解决方案,它允许我们在不修改源代码、不重启应用的情况下,对运行中的JVM进行字节码增强,实现无痕调试。

什么是 Java Agent?
Java Agent是Java SE 5引入的一项技术,它本质上是一个特殊的JAR文件,能够在主程序运行前或运行时动态修改类的字节码。这种特性使得Java Agent非常适合实现:
1.应用监控与性能分析
2.线上问题诊断与调试
3.AOP编程(无侵入式)

4.代码覆盖率分析


Java Agent有两种加载方式:
1.启动时加载:通过-javaagent参数指定,在JVM启动时加载

2.运行时加载:通过Attach API动态附加到运行中的JVM进程


技术选型
实现Java Agent需要操作字节码,目前主流的字节码操作库有:
ASM:轻量级、高性能,直接操作字节码指令
Javassist:更高层次的API,支持源码级别的修改
CGLIB:基于ASM,主要用于生成代理类
考虑到性能和灵活性,我们选择ASM作为字节码操作库,它能让我们更精细地控制字节码生成过程,适合生产环境使用。

实现步骤
依赖配置
<?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.yian</groupId>
    <artifactId>springboot-debug-agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <asm.version>9.3</asm.version>
    </properties>

    <dependencies>
        <!-- ASM字节码操作库 -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>${asm.version}</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>${asm.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <!-- 启动时加载的入口类 -->
                            <Premain-Class>com.yian.agent.DebugAgent</Premain-Class>
                            <!-- 运行时加载的入口类 -->
                            <Agent-Class>com.yian.agent.DebugAgent</Agent-Class>
                            <!-- 允许重定义类 -->
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <!-- 允许重转换类 -->
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project> 
Premain-Class:指定启动时加载Agent的入口类
Agent-Class:指定运行时加载Agent的入口类

Can-Redefine-Classes和Can-Retransform-Classes:允许Agent重定义和转换类


实现 Agent 入口类
Agent入口类需要实现premain(启动时加载)和agentmain(运行时加载)方法:
/**
 * Java Agent入口类,实现无痕调试注入功能
 */
public class DebugAgent {
    private static final Logger logger = Logger.getLogger(DebugAgent.class.getName());
    private static Instrumentation instrumentation;

    /**
     * JVM启动时加载Agent的入口方法
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        logger.info("DebugAgent premain 启动...");
        initialize(agentArgs, inst);
    }

    /**
     * 运行时动态加载Agent的入口方法
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        logger.info("DebugAgent agentmain 启动...");
        initialize(agentArgs, inst);
        // 堆代码 duidaima.com
        // 运行时加载需要触发类重转换
        try {
            Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
            for (Class<?> clazz : allLoadedClasses) {
                if (AgentConfig.shouldTransform(clazz.getName())) {
                    inst.retransformClasses(clazz);
                    logger.info("已重新转换类: " + clazz.getName());
                }
            }
        } catch (Exception e) {
            logger.severe("类重转换失败: " + e.getMessage());
        }
    }

    /**
     * 初始化Agent
     */
    private static void initialize(String agentArgs, Instrumentation inst) {
        instrumentation = inst;
        // 解析Agent参数
        AgentConfig.parse(agentArgs);
        // 添加类转换器
        inst.addTransformer(new MethodMonitorTransformer(), true);
        logger.info("DebugAgent 初始化完成,配置: " + AgentConfig.getConfigInfo());
    }

    public static Instrumentation getInstrumentation() {
        return instrumentation;
    }
}
入口类的核心职责:
1.接收并解析Agent参数
2.初始化Instrumentation实例
3.注册类转换器

4.运行时加载时触发类重转换


配置解析实现
我们需要灵活的配置机制,让用户可以指定需要监控的类和方法:
/**
 * Agent配置类,解析和存储注入规则
 */
public class AgentConfig {
    private static final Logger logger = Logger.getLogger(AgentConfig.class.getName());
    
    // 包含规则(正则表达式)
    private static final List<Pattern> includePatterns = new ArrayList<>();
    // 排除规则(正则表达式)
    private static final List<Pattern> excludePatterns = new ArrayList<>();
    // 日志文件路径
    private static String logFile;
    
    /**
     * 解析Agent参数
     * 格式: include=com.yian.*;exclude=com.yian.test.*;logFile=/tmp/agent.log
     */
    public static void parse(String agentArgs) {
        if (agentArgs == null || agentArgs.trim().isEmpty()) {
            logger.info("未指定Agent参数,使用默认配置");
            // 添加默认规则,监控所有Spring组件
            includePatterns.add(Pattern.compile("com\\.yian\\..*"));
            return;
        }
        
        String[] configItems = agentArgs.split(";");
        for (String item : configItems) {
            String[] keyValue = item.split("=", 2);
            if (keyValue.length != 2) continue;
            
            String key = keyValue[0].trim();
            String value = keyValue[1].trim();
            
            switch (key) {
                case"include":
                    includePatterns.add(Pattern.compile(convertToRegex(value)));
                    break;
                case"exclude":
                    excludePatterns.add(Pattern.compile(convertToRegex(value)));
                    break;
                case"logFile":
                    logFile = value;
                    break;
                default:
                    logger.warning("未知的配置项: " + key);
            }
        }
        
        // 如果没有指定包含规则,添加默认规则
        if (includePatterns.isEmpty()) {
            includePatterns.add(Pattern.compile("com\\.yian\\..*"));
        }
    }
    
    /**
     * 将通配符表达式转换为正则表达式
     */
    private static String convertToRegex(String wildcard) {
        return wildcard.replace(".", "\\.").replace("*", ".*").replace("?", ".");
    }
    
    /**
     * 判断类是否需要被转换
     */
    public static boolean shouldTransform(String className) {
        // 将类名转换为全限定名格式(例如:com/yian/MyClass -> com.yian.MyClass)
        String qualifiedName = className.replace("/", ".");
        
        // 检查是否匹配排除规则
        for (Pattern pattern : excludePatterns) {
            if (pattern.matcher(qualifiedName).matches()) {
                returnfalse;
            }
        }
        
        // 检查是否匹配包含规则
        for (Pattern pattern : includePatterns) {
            if (pattern.matcher(qualifiedName).matches()) {
                returntrue;
            }
        }
        
        returnfalse;
    }
    
    public static String getLogFile() {
        return logFile;
    }
    
    public static String getConfigInfo() {
        return String.format("include=%s, exclude=%s, logFile=%s",
                includePatterns, excludePatterns, logFile);
    }
}
.解析命令行参数(include/exclude规则、日志路径)
.通配符到正则表达式的转换(方便用户使用*和?通配符)

.类匹配逻辑(决定哪些类需要被增强)


字节码转换器实现
类转换器是Agent的核心,它决定了如何修改类的字节码:
/**
 * 类转换器,负责将监控逻辑注入到目标类中
 */
public class MethodMonitorTransformer implements ClassFileTransformer {
    private static final Logger logger = Logger.getLogger(MethodMonitorTransformer.class.getName());
    
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                           ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 判断是否需要转换该类
        if (!AgentConfig.shouldTransform(className)) {
            return null;
        }
        
        try {
            logger.info("开始转换类: " + className);
            
            // 读取类字节码
            ClassReader cr = new ClassReader(classfileBuffer);
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
            
            // 使用自定义的ClassVisitor处理类
            DebugClassVisitor cv = new DebugClassVisitor(cw, className.replace("/", "."));
            
            // 处理类,SKIP_DEBUG可以提高性能,不处理调试信息
            cr.accept(cv, ClassReader.SKIP_DEBUG);
            
            // 返回修改后的字节码
            return cw.toByteArray();
        } catch (Exception e) {
            logger.severe("转换类 " + className + " 失败: " + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }
}
转换器的工作流程:
.检查类是否需要被转换(基于AgentConfig的规则)
.使用ASM的ClassReader读取类字节码
.创建ClassWriter用于生成修改后的字节码
.使用自定义的ClassVisitor处理类结构

.返回修改后的字节码


字节码增强实现
最后,我们需要实现具体的字节码增强逻辑,在方法调用前后插入监控代码:
/**
 * 自定义ClassVisitor,用于访问类的方法并注入监控逻辑
 */
public class DebugClassVisitor extends ClassVisitor {
    private final String className;
    
    public DebugClassVisitor(ClassVisitor classVisitor, String className) {
        super(Opcodes.ASM9, classVisitor);
        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 mv;
        }
        
        // 过滤掉native方法
        if ((access & Opcodes.ACC_NATIVE) != 0) {
            return mv;
        }
        
        // 使用自定义的MethodVisitor处理方法
        return new DebugMethodVisitor(mv, className, name, descriptor, access);
    }
}
/**
 * 自定义MethodVisitor,用于在方法执行前后注入监控逻辑
 */
public class DebugMethodVisitor extends AdviceAdapter {
    private static final Logger logger = Logger.getLogger(DebugMethodVisitor.class.getName());
    
    private final String className;
    private final String methodName;
    private final String methodDesc;
    
    // 局部变量索引,用于存储方法开始时间
    private int startTimeVar;
    // 用于标识是否是静态方法
    private final boolean isStatic;

    protected DebugMethodVisitor(MethodVisitor methodVisitor, String className, 
                                String methodName, String methodDesc, int access) {
        super(Opcodes.ASM9, methodVisitor, access, methodName, methodDesc);
        this.className = className;
        this.methodName = methodName;
        this.methodDesc = methodDesc;
        this.isStatic = (access & Opcodes.ACC_STATIC) != 0;
    }
    
    /**
     * 在方法进入时插入代码
     */
    @Override
    protected void onMethodEnter() {
        // 记录方法开始时间
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        startTimeVar = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, startTimeVar);
        
        // 打印方法调用信息
        mv.visitLdcInsn(className);
        mv.visitLdcInsn(methodName);
        mv.visitMethodInsn(INVOKESTATIC, "com/yian/agent/MethodMonitor", 
                         "logMethodEnter", "(Ljava/lang/String;Ljava/lang/String;)V", false);
        
        // 打印方法参数
        printParameters();
    }
    
    /**
     * 打印方法参数
     */
    private void printParameters() {
        Type[] argumentTypes = Type.getArgumentTypes(methodDesc);
        
        // 创建参数数组
        mv.visitIntInsn(BIPUSH, argumentTypes.length);
        mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
        int paramsArrayVar = newLocal(Type.getType(Object[].class));
        mv.visitVarInsn(ASTORE, paramsArrayVar);
        
        // 填充参数数组
        int paramIndex = isStatic ? 0 : 1; // 非静态方法第一个参数是this
        for (int i = 0; i < argumentTypes.length; i++) {
            Type type = argumentTypes[i];
            int size = type.getSize();
            
            // 加载数组和索引
            mv.visitVarInsn(ALOAD, paramsArrayVar);
            mv.visitIntInsn(BIPUSH, i);
            
            // 加载参数值并装箱
            loadArg(paramIndex);
            box(type);
            
            // 存入数组
            mv.visitInsn(AASTORE);
            
            paramIndex += size;
        }
        
        // 调用日志方法记录参数
        mv.visitLdcInsn(className);
        mv.visitLdcInsn(methodName);
        mv.visitVarInsn(ALOAD, paramsArrayVar);
        mv.visitMethodInsn(INVOKESTATIC, "com/yian/agent/MethodMonitor", 
                         "logMethodParameters", "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V", false);
    }
    
    /**
     * 在方法退出时插入代码
     */
    @Override
    protected void onMethodExit(int opcode) {
        // 计算方法执行时间
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(LLOAD, startTimeVar);
        mv.visitInsn(LSUB);
        int durationVar = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, durationVar);
        
        // 如果是正常返回,记录返回值
        if ((opcode >= IRETURN && opcode <= RETURN)) {
            // 复制返回值到栈顶(因为onMethodExit时返回值已经在栈上)
            if (opcode != RETURN) { // 不是void返回
                dupReturnValue(opcode);
                box(Type.getReturnType(methodDesc));
                
                // 记录返回值
                mv.visitLdcInsn(className);
                mv.visitLdcInsn(methodName);
                mv.visitVarInsn(LLOAD, durationVar);
                mv.visitMethodInsn(INVOKESTATIC, "com/yian/agent/MethodMonitor", 
                                 "logMethodReturn", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/Object;)V", false);
            } else {
                // void返回
                mv.visitLdcInsn(className);
                mv.visitLdcInsn(methodName);
                mv.visitVarInsn(LLOAD, durationVar);
                mv.visitInsn(ACONST_NULL);
                mv.visitMethodInsn(INVOKESTATIC, "com/yian/agent/MethodMonitor", 
                                 "logMethodReturn", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/Object;)V", false);
            }
        } 
        // 如果是异常退出,记录异常信息
        elseif (opcode == ATHROW) {
            // 复制异常引用
            mv.visitInsn(DUP);
            
            // 记录异常
            mv.visitLdcInsn(className);
            mv.visitLdcInsn(methodName);
            mv.visitVarInsn(LLOAD, durationVar);
            mv.visitMethodInsn(INVOKESTATIC, "com/yian/agent/MethodMonitor", 
                             "logMethodException", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/Throwable;)V", false);
        }
    }
    
    /**
     * 复制返回值到栈顶
     */
    private void dupReturnValue(int opcode) {
        switch (opcode) {
            case IRETURN:
            case FRETURN:
                mv.visitInsn(DUP);
                break;
            case LRETURN:
            case DRETURN:
                mv.visitInsn(DDUP);
                break;
            case ARETURN:
                mv.visitInsn(DUP);
                break;
            default:
                // 不处理void返回
        }
    }
}  
/**
 * 方法监控工具类,提供日志记录功能
 */
public class MethodMonitor {
    private static final Logger logger = Logger.getLogger(MethodMonitor.class.getName());
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    private static FileWriter logWriter;
    
    static {
        // 初始化日志写入器
        String logFile = AgentConfig.getLogFile();
        if (logFile != null && !logFile.isEmpty()) {
            try {
                File file = new File(logFile);
                // 创建父目录
                if (!file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                }
                // 追加模式
                logWriter = new FileWriter(file, true);
            } catch (IOException e) {
                logger.severe("初始化日志文件失败: " + e.getMessage());
                logWriter = null;
            }
        }
    }
    
    /**
     * 记录方法进入
     */
    public static void logMethodEnter(String className, String methodName) {
        String message = String.format("[%s] 方法进入: %s.%s()", 
                DATE_FORMAT.format(new Date()), className, methodName);
        writeLog(message);
    }
    
    /**
     * 记录方法参数
     */
    public static void logMethodParameters(String className, String methodName, Object[] params) {
        StringBuilder sb = new StringBuilder();
        sb.append("[参数]: ");
        if (params != null) {
            for (int i = 0; i < params.length; i++) {
                sb.append("param").append(i).append("=")
                  .append(params[i] != null ? params[i].toString() : "null");
                if (i < params.length - 1) {
                    sb.append(", ");
                }
            }
        } else {
            sb.append("无参数");
        }
        
        String message = String.format("[%s] %s.%s(): %s", 
                DATE_FORMAT.format(new Date()), className, methodName, sb.toString());
        writeLog(message);
    }
    
    /**
     * 记录方法返回
     */
    public static void logMethodReturn(String className, String methodName, long duration, Object returnValue) {
        String returnStr = returnValue != null ? returnValue.toString() : "void";
        String message = String.format("[%s] 方法返回: %s.%s() 执行时间: %dms, 返回值: %s", 
                DATE_FORMAT.format(new Date()), className, methodName, duration, returnStr);
        writeLog(message);
    }
    
    /**
     * 记录方法异常
     */
    public static void logMethodException(String className, String methodName, long duration, Throwable throwable) {
        String exceptionMsg = throwable != null ? throwable.getClass().getName() + ": " + throwable.getMessage() : "未知异常";
        String message = String.format("[%s] 方法异常: %s.%s() 执行时间: %dms, 异常: %s", 
                DATE_FORMAT.format(new Date()), className, methodName, duration, exceptionMsg);
        writeLog(message);
        
        // 打印异常堆栈
        if (throwable != null) {
            throwable.printStackTrace();
        }
    }
    
    /**
     * 写入日志到文件或控制台
     */
    private static void writeLog(String message) {
        System.out.println(message); // 同时输出到控制台
        
        if (logWriter != null) {
            try {
                logWriter.write(message + "\n");
                logWriter.flush();
            } catch (IOException e) {
                logger.severe("写入日志失败: " + e.getMessage());
            }
        }
    }
}
字节码增强的核心逻辑:
1.使用DebugClassVisitor遍历类中的所有方法
2.对每个非构造方法使用DebugMethodVisitor进行处理
3.在方法进入时(onMethodEnter):
  3.1 记录开始时间
  3.2 打印方法调用信息
  3.3 收集并打印方法参数
4.在方法退出时(onMethodExit):
  4.1计算执行时间
  4.2记录返回值(正常退出)
  4.3记录异常信息(异常退出)
MethodMonitor类提供了实际的日志记录功能,支持输出到控制台和文件。

使用方法
在启动SpringBoot应用时通过-javaagent参数指定Agent:
java -javaagent:/path/to/springboot-debug-agent-1.0-SNAPSHOT-jar-with-dependencies.jar="include=com.yian.service.*;logFile=/tmp/debug.log" -jar your-springboot-app.jar
对于已经运行的应用,可以使用jattach工具动态附加Agent:
# 安装jattach(如果未安装)
# Ubuntu: sudo apt install jattach
# CentOS: yum install jattach

# 动态附加Agent
jattach <pid> load instrument false "/path/to/springboot-debug-agent-1.0-SNAPSHOT-jar-with-dependencies.jar=include=com.yian.controller.*"

用户评论