You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 00:50:24