premain里没注册classfiletransformer就等于没写agent;必须通过instrumentation.addtransformer注册转换器,且manifest.mf中需正确声明premain-class和can-retransform-classes。

premain 里没注册 transformer 就等于没写 agent
Java Agent 要起作用,premain 方法必须拿到 Instrumentation 实例,并调用 addTransformer 注册一个 ClassFileTransformer。漏掉这一步,JVM 根本不会触发你的字节码改写逻辑。
常见错误包括:
-
premain方法签名不对(必须是public static void premain(String agentArgs, Instrumentation inst)) - MANIFEST.MF 中缺失
Premain-Class,或值写错类名(比如少个包前缀) - 注册时传了
false给canRetransform,但后续又想对 Spring Bean 做增强——直接抛UnsupportedOperationException
确保 Can-Retransform-Classes: true 和 Can-Redefine-Classes: true 都在 MANIFEST.MF 里声明,否则已加载类无法重定义。
transform 方法里不能打日志、不能做耗时操作
transform 是类加载关键路径上的钩子,所有类(包括 java.lang.*)都会经过它。这里慢 1ms,整个应用启动可能拖慢几秒。
必须遵守的约束:
- 只对目标类过滤:用
className.startsWith("com.yourcompany.")快速跳过无关类,别用正则或反射判断 - 别调用
System.out.println或 SLF4J 的logger.info—— 日志框架本身类可能还没加载完,会引发死锁或NoClassDefFoundError - 避免在
transform里 new 对象、解析 JSON、查配置中心——这些都该提前初始化好,放到静态 final 字段里复用
ASM 的 ClassReader + ClassWriter + 自定义 MethodVisitor 是标准链路,但 visitor 里只做字节码插入,不执行业务逻辑。
插桩点选错会导致耗时不准甚至崩溃
统计方法耗时,核心是在入口压入 System.nanoTime(),出口取差值。但“出口”不是简单找 return 指令——方法可能有多个返回点(正常 return、异常 throw、finally 块里的 return)。
正确做法是:
- 在
visitCode()后立即插入计时开始逻辑(visitInsn(Opcodes.LDC)+visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false)) - 覆盖所有返回指令:
IRETURN、LRETURN、FRETURN、DRETURN、ARETURN、RETURN,以及ATHROW(异常出口也要统计) - 别用
System.currentTimeMillis()—— 它受系统时钟调整影响,nanoTime()才是单调递增的高精度时钟
如果目标方法本身有 try-catch-finally,插桩后需确保 finally 块里的 return 不会覆盖主路径的耗时上报逻辑,否则统计值会偏小。
retransformClasses 对已加载类生效的前提很苛刻
Spring Boot 应用启动时,Controller、Service 大多在 Agent 初始化前就完成了加载。这时 transform 不会触发,必须手动调用 retransformClasses。
但要注意:
- 只能重定义方法体,不能改方法签名、增删字段、加注解——违反会抛
UnmodifiableClassException - 如果目标方法当前有活跃栈帧(比如正在执行 HTTP 请求处理),JVM 会等它退出后再替换字节码,期间仍走旧逻辑
- 要先调用
instrumentation.getAllLoadedClasses()扫描匹配类,再批量传给retransformClasses,别一个个调用,性能差且易超时
生产环境慎用 retransformClasses 全量扫描,建议配合白名单机制,只对明确需要监控的类做重定义,避免触发 JVM 内部同步锁导致 STW 时间延长。
真正难的不是插桩,而是让插桩逻辑在 GC 压力大、类加载频繁、存在动态代理的生产环境里稳定跑通。比如 JDK 17+ 的 sealed class、record 类的 descriptor 解析容易出错;Spring AOP 生成的代理类名带 $EnhancerBySpringCGLIB,得在过滤时兼容;还有 java.lang.invoke.MethodHandle 这类底层调用,插桩后可能破坏 JVM 内联优化。这些细节不验证,上线就抖。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











