4.代码覆盖率分析
2.运行时加载:通过Attach API动态附加到运行中的JVM进程
<?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的入口类
Can-Redefine-Classes和Can-Retransform-Classes:允许Agent重定义和转换类
/** * 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; } }入口类的核心职责:
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规则、日志路径)
.类匹配逻辑(决定哪些类需要被增强)
/** * 类转换器,负责将监控逻辑注入到目标类中 */ 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; } } }转换器的工作流程:
.返回修改后的字节码
/** * 自定义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()); } } } }字节码增强的核心逻辑:
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.*"