两个Java Agent冲突导致Throwable类getMessage()方法增强丢失问题求助
问题描述
我需要对java.lang.Throwable的getMessage()方法做增强,输出自定义日志,自己写的FirstAgent单独运行完全正常。但当它和另一个厂商的Agent(我用SecondAgent模拟了这个场景)一起附加到JVM时,我的增强效果就丢失了。单独跑两个Agent都没问题,但一起跑的时候,先加载的FirstAgent对Throwable的修改会被后加载的SecondAgent覆盖——第二个Agent加载Throwable时用的是原始类的字节码,不是第一个Agent修改后的版本,然后它自己增强后就把之前的修改冲掉了。
我是不是哪里操作错了?求各位大佬给点建议!
复现代码
FirstAgent.java
import java.io.ByteArrayInputStream; import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.jar.JarFile; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; public class FirstAgent implements ClassFileTransformer { public static void premain(String agentArgs, Instrumentation inst) { try { String agentJarLocation = new File( FirstAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarLocation)); } catch (Exception e2) { e2.printStackTrace(); } try { Class<?> targetClazz = Class.forName("java.lang.Throwable"); FirstAgent fa = new FirstAgent(); try { inst.addTransformer(fa, true); inst.retransformClasses(targetClazz); } catch (Exception ex) { throw new RuntimeException("Failed to transform [" + targetClazz.getName() + "]", ex); } finally { inst.removeTransformer(fa); } } catch (Throwable ex) { ex.printStackTrace(); } } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { byte[] byteCode = classfileBuffer; try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod[] methods = ctClass.getDeclaredMethods(); for (CtMethod method : methods) { if (method.getName().contains("getMessage") && Modifier.isPublic(method.getModifiers())) { method.insertBefore("System.out.println(\"First Custome Message\");"); } } byteCode = ctClass.toBytecode(); ctClass.detach(); } catch (Throwable ex) { ex.printStackTrace(); } return byteCode; } }
SecondAgent.java
import java.io.ByteArrayInputStream; import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.jar.JarFile; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; public class SecondAgent implements ClassFileTransformer { public static void premain(String agentArgs, Instrumentation inst) { try { String agentJarLocation = new File( SecondAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarLocation)); } catch (Exception e2) { e2.printStackTrace(); } try { Class<?> targetClazz = Class.forName("java.lang.Throwable"); SecondAgent sa = new SecondAgent(); try { inst.addTransformer(sa, true); inst.retransformClasses(targetClazz); } catch (Exception ex) { throw new RuntimeException("Failed to transform [" + targetClazz.getName() + "]", ex); } finally { inst.removeTransformer(sa); } } catch (Throwable ex) { ex.printStackTrace(); } } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { byte[] byteCode = classfileBuffer; try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod[] methods = ctClass.getDeclaredMethods(); for (CtMethod method : methods) { if (method.getName().contains("getMessage") && Modifier.isPublic(method.getModifiers())) { method.insertBefore("System.out.println(\"Secocnd Custome Message\");"); } } byteCode = ctClass.toBytecode(); ctClass.detach(); } catch (Throwable ex) { ex.printStackTrace(); } return byteCode; } }
问题分析与解决方案
核心问题原因
你遇到的问题本质是Java Agent的类重转换机制和Transformer生命周期管理不当:
- Transformer被提前移除:每个Agent在做完
retransformClasses后立刻调用removeTransformer,导致JVM中没有保留你的Transformer逻辑。后续Agent触发重转换时,无法基于你的修改继续处理,只能拿到原始类字节码重新增强,直接覆盖之前的修改。 - 重转换的字节码来源:当调用
retransformClasses时,JVM会将类的原始字节码(从类加载器的原始来源读取)传入Transformer,而不是当前已加载的修改后的字节码——除非Transformer链中有之前的Transformer处理过这个类。
具体修复步骤
1. 保留Transformer,维持链式执行
Java Agent的Transformer是链式工作的:前一个Transformer输出的字节码会作为后一个Transformer的输入。所以不要在增强后立刻移除Transformer,让后续Agent能基于你的修改继续处理。
2. 优化类加载与重转换逻辑
避免主动调用Class.forName提前加载目标类(除非你确定它还没被加载),而是让Transformer在类加载/重转换时自动触发。如果类已经被加载,再触发重转换即可。
3. 修改Transformer的匹配逻辑
把method.getName().contains("getMessage")改成精确匹配"getMessage",避免误增强其他包含该字符串的方法(比如getLocalizedMessage)。
修改后的示例代码(FirstAgent)
import java.io.ByteArrayInputStream; import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.jar.JarFile; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; public class FirstAgent implements ClassFileTransformer { public static void premain(String agentArgs, Instrumentation inst) { try { // 将Agent Jar加入Bootstrap类加载器搜索路径,确保Javassist能处理核心类 String agentJarLocation = new File( FirstAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath(); inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarLocation)); // 添加Transformer,不移除,保持链式执行 FirstAgent fa = new FirstAgent(); inst.addTransformer(fa, true); // 如果Throwable已经被加载,触发重转换 try { Class<?> targetClazz = Class.forName("java.lang.Throwable"); inst.retransformClasses(targetClazz); } catch (Exception ex) { throw new RuntimeException("Failed to retransform Throwable", ex); } } catch (Throwable ex) { ex.printStackTrace(); } } @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 只处理java.lang.Throwable类 if (!"java/lang/Throwable".equals(className)) { return classfileBuffer; } byte[] byteCode = classfileBuffer; try { ClassPool classPool = ClassPool.getDefault(); // 基于传入的当前字节码创建CtClass,确保是最新的修改版本 CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod[] methods = ctClass.getDeclaredMethods(); for (CtMethod method : methods) { // 精确匹配方法名,避免误增强 if ("getMessage".equals(method.getName()) && Modifier.isPublic(method.getModifiers())) { method.insertBefore("System.out.println(\"First Custome Message\");"); } } byteCode = ctClass.toBytecode(); ctClass.detach(); } catch (Throwable ex) { ex.printStackTrace(); } return byteCode; } }
SecondAgent也需要做完全相同的修改:移除finally块中的removeTransformer调用,精确匹配方法名。
额外注意事项
- Agent加载顺序:JVM会按照Agent的启动顺序执行Transformer,如果你需要你的Agent优先增强,要确保它先被附加到JVM。
- 核心类增强风险:增强
java.lang包下的类要格外小心,避免引入副作用(比如死循环、破坏类结构),建议在测试环境充分验证。 - Javassist版本兼容性:确保你使用的Javassist版本与目标JVM版本兼容,避免出现字节码格式不兼容的问题。
备注:内容来源于stack exchange,提问作者Sri

