闽公网安备 35020302035485号
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.*"