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

如何用Javassist ExprEditor区分同一类型对象的多方法调用?

问题描述

需要通过Javassist的ExprEditor结合Prometheus Java客户端,按实例(sc1、sc2)统计SomeClass中doThis()和doOther()的调用次数,但当前无法区分这两个实例的调用。

示例目标类:

public class InstrumentedClass {

    private SomeClass sc1 = new SomeClass();
    private SomeClass sc2 = new SomeClass();

    public void doSomething() {
        sc1.doThis();
        sc2.doOther();
        sc2.doThis();
        sc1.doOther();
        // 调用次数和顺序不固定,无法依赖行号(如insertAt())
    }
}

当前Instrumentation代码(无法区分实例):

cc.getDeclaredMethod("doSomething").instrument(new ExprEditor() {
    public void edit(MethodCall m) throws CannotCompileException {
        if (m.getClassName().equals("SomeClass")) {
            if (m.getMethodName().equals("doThis")) {

                m.replace("{ $_ = $proceed($$); ((io.prometheus.metrics.core.datapoints.CounterDataPoint) " +
                    "counterDoThis.labelValues(new java.lang.String[] {\"MissingPiece\"})).inc(1.0); }");

            } else if (m.getMethodName().equals("doOther")) {

                m.replace("{ $_ = $proceed($$); ((io.prometheus.metrics.core.datapoints.CounterDataPoint) " +
                    "counterdoOther.labelValues(new java.lang.String[] {\"MissingPiece\"})).inc(1.0); }");

            }
        }
    }
});

核心困境:

  • 两个实例类型相同,无法通过类型区分
  • 对象ID易变且跨重启无意义,不适合作为标签
  • 担心字节码中丢失实例标识信息,无法实现按实例统计

可行解决方案

1. 直接通过调用目标的字段名称识别

Javassist的MethodCall对象可以获取调用的目标表达式字符串,对于sc1.doThis()这类调用,目标表达式就是sc1,直接提取该字符串作为Prometheus标签值即可。

修改后的Instrumentation代码:

cc.getDeclaredMethod("doSomething").instrument(new ExprEditor() {
    public void edit(MethodCall m) throws CannotCompileException {
        if (m.getClassName().equals("SomeClass")) {
            // 获取调用目标的字段名称(如sc1、sc2)
            String instanceTag = m.getTarget().toString();
            // 仅处理已知的实例字段
            if (instanceTag.equals("sc1") || instanceTag.equals("sc2")) {
                if (m.getMethodName().equals("doThis")) {
                    m.replace("{ $_ = $proceed($$); ((io.prometheus.metrics.core.datapoints.CounterDataPoint) " +
                        "counterDoThis.labelValues(new java.lang.String[] {\"" + instanceTag + "\"})).inc(1.0); }");
                } else if (m.getMethodName().equals("doOther")) {
                    m.replace("{ $_ = $proceed($$); ((io.prometheus.metrics.core.datapoints.CounterDataPoint) " +
                        "counterdoOther.labelValues(new java.lang.String[] {\"" + instanceTag + "\"})).inc(1.0); }");
                }
            }
        }
    }
});

优缺点:

  • 优点:无需修改目标类(SomeClass、InstrumentedClass)的原有代码,实现简单直接
  • 缺点:依赖编译后的目标表达式字符串,若JVM或编译器做了激进优化(如字段内联)可能失效,但常规场景下稳定可用

2. 给实例注入标识字段

通过Javassist给SomeClass动态添加一个标识字段,在InstrumentedClass初始化sc1、sc2时为该字段赋值,后续方法调用时读取该字段作为标签。

步骤1:给SomeClass添加标识字段

CtClass someClass = ClassPool.getDefault().get("SomeClass");
// 添加私有字符串字段存储实例标识
CtField instanceIdField = new CtField(CtClass.stringType, "instanceId", someClass);
instanceIdField.setModifiers(Modifier.PRIVATE);
someClass.addField(instanceIdField);
// 添加getter方法(可选,也可直接通过反射读取)
someClass.addMethod(CtNewMethod.getter("getInstanceId", instanceIdField));

步骤2:修改InstrumentedClass的实例初始化代码

CtClass instrumentedClass = cc.getDeclaredClass("InstrumentedClass");
// 修改sc1的初始化代码,添加标识赋值
CtField sc1Field = instrumentedClass.getDeclaredField("sc1");
sc1Field.setInitializer(CtNewExpr.newInstance(someClass, new CtClass[0], new CtExpression[0]));
instrumentedClass.addInitializer("this.sc1.setInstanceId(\"sc1\");");
// 同理处理sc2
CtField sc2Field = instrumentedClass.getDeclaredField("sc2");
sc2Field.setInitializer(CtNewExpr.newInstance(someClass, new CtClass[0], new CtExpression[0]));
instrumentedClass.addInitializer("this.sc2.setInstanceId(\"sc2\");");

步骤3:在方法调用时读取标识

cc.getDeclaredMethod("doSomething").instrument(new ExprEditor() {
    public void edit(MethodCall m) throws CannotCompileException {
        if (m.getClassName().equals("SomeClass")) {
            String methodName = m.getMethodName();
            // 读取目标实例的标识
            m.replace("{ $_ = $proceed($$); " +
                "String instanceTag = $0.getInstanceId(); " +
                (methodName.equals("doThis") ? 
                    "counterDoThis.labelValues(instanceTag).inc();" : 
                    "counterdoOther.labelValues(instanceTag).inc();") +
                "}");
        }
    }
});

优缺点:

  • 优点:不依赖编译后的表达式字符串,稳定性更高,支持更复杂的实例标识逻辑
  • 缺点:需要修改目标类的字节码,增加了一定的复杂度

3. 通过字节码字段索引识别实例

如果调用目标的表达式字符串不可靠,可以通过分析字节码中的字段引用索引,映射到对应的字段名称。

cc.getDeclaredMethod("doSomething").instrument(new ExprEditor() {
    public void edit(MethodCall m) throws CannotCompileException {
        if (m.getClassName().equals("SomeClass")) {
            // 获取调用目标的字段引用
            FieldRef fieldRef = (FieldRef) m.getTarget().getClassFile2().getConstPool().getConstInfo(m.getTarget().getIndex());
            // 获取字段名称(如sc1、sc2)
            String instanceTag = fieldRef.getName();
            if (instanceTag.equals("sc1") || instanceTag.equals("sc2")) {
                // 后续逻辑同方案1,使用instanceTag作为标签
                if (m.getMethodName().equals("doThis")) {
                    m.replace("{ $_ = $proceed($$); counterDoThis.labelValues(\"" + instanceTag + "\").inc(); }");
                } else if (m.getMethodName().equals("doOther")) {
                    m.replace("{ $_ = $proceed($$); counterdoOther.labelValues(\"" + instanceTag + "\").inc(); }");
                }
            }
        }
    }
});

优缺点:

  • 优点:直接从字节码常量池获取字段信息,不受编译优化影响,最可靠
  • 缺点:代码相对复杂,需要熟悉Javassist的字节码底层API

内容的提问来源于stack exchange,提问作者autopwn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 14:45:09