Java方法调用追踪问题:Getter方法未被Javassist插装及替代方案咨询
问题
我正在使用Javassist库实现Java方法调用追踪,编写了如下代码:
for (CtMethod meth : methods) { String method = meth.getName(); try { meth.instrument(new ExprEditor() { public void edit(MethodCall m) throws CannotCompileException { StringBuffer buffer = new StringBuffer(); StringBuffer bufferEnd = new StringBuffer(); String formatMessage = "String instMessage=new java.text.MessageFormat(\"{0},{1},{2}, has started execution,{3},{4}\\").format(new String[]{\\"" + "returnType" + " " + meth.getLongName() + "\\"},\\"" + cc.getSimpleName() + "\\"},\\"" + method + "\\"}, new java.util.Date().toString()" + ",java.lang.Thread.currentThread().getId()+\\"\\"});"; buffer.append(formatMessage); buffer.append("org.springframework.samples.jpetstore.domain.logic.Log.trace( instMessage);"); String formatMessageEnd = "String instMessageEnd=new java.text.MessageFormat(\"{0},{1},{2}, has finished execution,{3},{4}\\").format(new String[]{\\"" +"returnType" + " " + meth.getLongName() + "\\"},\\"" + cc.getSimpleName() + "\\"},\\"" + method + "\\"}, new java.util.Date().toString()" + ",java.lang.Thread.currentThread().getId()+\\"\\"});"; bufferEnd.append(formatMessageEnd); bufferEnd.append("org.springframework.samples.jpetstore.domain.logic.Log.trace(instMessageEnd);"); String start = buffer.toString(); String stop = bufferEnd.toString(); m.replace("{ try {" + start + " $_ = $proceed($$); } finally { " + stop + " } }"); } }); } catch (CannotCompileException e) { System.out.println("******error:" + cc.getSimpleName() + " - " + method); System.out.println(e.getMessage()); e.printStackTrace(); } }
该代码可正常对普通方法进行插装,但无法处理Getter方法(例如类A中的getB()方法),此类方法仍保持原有未插装状态。现寻求解决该Getter方法插装问题的方案,同时希望了解不使用AOP的其他Java方法调用追踪方案,以实现类似如下格式的调用与返回日志输出:
ClassA.method() **called** ClassB.method() at date() ClassB.method() **return to** ClassA.method() at date()
解决方案
一、解决Getter方法插装失败问题
1. 修正插装代码的语法错误
你的代码中字符串转义逻辑存在错误,导致生成的字节码编译失败(Getter方法因代码简单更容易触发此类错误),最终被catch块跳过插装。修正后的插装逻辑如下:
for (CtMethod meth : methods) { String methodName = meth.getName(); try { meth.instrument(new ExprEditor() { @Override public void edit(MethodCall m) throws CannotCompileException { // 前置日志:记录调用发起 String startLog = String.format( "String caller = \"%s\"; " + "String callee = \"%s\"; " + "String timestamp = new java.util.Date().toString(); " + "org.springframework.samples.jpetstore.domain.logic.Log.trace(caller + \" **called** \" + callee + \" at \" + timestamp);", meth.getLongName(), m.getMethod().getLongName() ); // 后置日志:记录调用返回 String endLog = String.format( "String caller = \"%s\"; " + "String callee = \"%s\"; " + "String timestamp = new java.util.Date().toString(); " + "org.springframework.samples.jpetstore.domain.logic.Log.trace(callee + \" **return to** \" + caller + \" at \" + timestamp);", meth.getLongName(), m.getMethod().getLongName() ); // 替换原方法调用为包含日志的逻辑 m.replace("{ try {" + startLog + " $_ = $proceed($$); } finally { " + endLog + " } }"); } }); } catch (CannotCompileException e) { System.err.println("插装失败:" + cc.getSimpleName() + "." + methodName); e.printStackTrace(); } }
2. 确保Getter方法被纳入插装范围
检查methods集合的获取逻辑:
- 若使用
cc.getDeclaredMethods(),确认未过滤get开头的方法; - 若需插装父类的Getter方法,改用
cc.getMethods()获取所有公有方法。
3. 处理JVM方法内联问题
Getter方法因逻辑简单,容易被JVM内联导致插装代码不执行,可通过以下方式解决:
- 开发环境:添加JVM启动参数
-XX:-Inline禁用内联; - 生产环境:直接插装Getter方法本身,而非仅处理调用处,强制保留插装代码:
meth.insertAround("{ " + "String callee = \"" + meth.getLongName() + "\"; " + "StackTraceElement callerStack = Thread.currentThread().getStackTrace()[1]; " + "String caller = callerStack.getClassName() + \".\" + callerStack.getMethodName(); " + "String timestamp = new java.util.Date().toString(); " + "org.springframework.samples.jpetstore.domain.logic.Log.trace(caller + \" **called** \" + callee + \" at \" + timestamp); " + "try { " + "$_ = $proceed($$); " + "} finally { " + "org.springframework.samples.jpetstore.domain.logic.Log.trace(callee + \" **return to** \" + caller + \" at \" + timestamp); " + "} " + "}");
二、非AOP的Java方法调用追踪方案
1. Javassist直接插装方法入口/出口
遍历目标类的所有方法,直接在方法开头插入调用日志,在方法结尾插入返回日志:
for (CtMethod meth : methods) { // 前置日志:记录被调用(通过栈轨迹获取调用者) meth.insertBefore("StackTraceElement callerStack = Thread.currentThread().getStackTrace()[2]; " + "String caller = callerStack.getClassName() + \".\" + callerStack.getMethodName(); " + "String callee = \"" + meth.getLongName() + "\"; " + "org.springframework.samples.jpetstore.domain.logic.Log.trace(caller + \" **called** \" + callee + \" at \" + new java.util.Date().toString());"); // 后置日志:记录返回 meth.insertAfter("StackTraceElement callerStack = Thread.currentThread().getStackTrace()[2]; " + "String caller = callerStack.getClassName() + \".\" + callerStack.getMethodName(); " + "String callee = \"" + meth.getLongName() + "\"; " + "org.springframework.samples.jpetstore.domain.logic.Log.trace(callee + \" **return to** \" + caller + \" at \" + new java.util.Date().toString());", true); }
2. Byte Buddy字节码操作库
Byte Buddy提供简洁的API实现动态插装,支持Java Agent方式在类加载时修改字节码:
// Java Agent 初始化逻辑 new AgentBuilder.Default() .type(ElementMatchers.any()) // 匹配所有类 .transform((builder, typeDescription, classLoader, module) -> builder.method(ElementMatchers.any()) // 匹配所有方法 .intercept(MethodDelegation.to(MethodTraceInterceptor.class)) ).installOn(instrumentation); // 日志拦截器类 public class MethodTraceInterceptor { @RuntimeType public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception { StackTraceElement callerStack = Thread.currentThread().getStackTrace()[3]; String caller = callerStack.getClassName() + "." + callerStack.getMethodName(); String callee = method.getDeclaringClass().getName() + "." + method.getName(); String timestamp = new Date().toString(); Log.trace(caller + " **called** " + callee + " at " + timestamp); try { return callable.call(); } finally { Log.trace(callee + " **return to** " + caller + " at " + timestamp); } } }
3. Java Instrumentation API + ASM
使用JVM原生Instrumentation接口结合ASM底层字节码操作,适合高性能场景:
- 编写Java Agent,在
premain方法中注册ClassFileTransformer; - 用ASM访问类的方法,在方法的
visitCode阶段插入调用日志,在visitInsn(返回指令)前插入返回日志。
4. 基于JVM TI的工具
JVM Tool Interface(JVM TI)提供底层JVM操作能力,可实现方法调用追踪,适合开发调试工具,但实现复杂度较高。
内容的提问来源于stack exchange,提问作者MuhammedH
相关产品推荐
相关产品推荐

