如何用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
相关产品推荐
相关产品推荐

