这是一个关于非常新功能的非常具体的问题。背景信息:我正在撰写关于 Java 中运行时代码生成的学士论文,并且正在开发探查器的原型,并在其中展示我的工作。在那里,我使用 Java ClassFile API 来操作类并注入字节码,例如用于测量方法调用或方法运行时的代码。由于一切都必须在程序运行时发生,因此我还需要在运行时更新类,这是通过 Instrumentation API 和相应的代理完成的。
当一个类中只需要完成一个转换时,它可以完美地工作,但是一旦出现多个转换,我就无法将它们组合起来,只应用一个而其他的被忽略。
这里是生成
CodeTransform
的代码,以便更好地理解:
private static CodeTransform generateMethodCodeTransform(int statisticsId, List<String> metricValues) {
return new CodeTransform() {
private int startSlot = 0;
private int endSlot = 0;
private int durationSlot = 0;
@Override
public void accept(CodeBuilder builder, CodeElement element) {
if(metricValues.contains(ProfilerMethodMetric.RUNTIME.toString())) {
if(element instanceof ReturnInstruction) {
builder.invokestatic(ClassDesc.of(System.class.getName()), "nanoTime", MethodTypeDesc.of(ConstantDescs.CD_long));
endSlot = builder.allocateLocal(TypeKind.LongType);
builder.lstore(endSlot);
builder.lload(endSlot);
builder.lload(startSlot);
builder.lsub();
durationSlot = builder.allocateLocal(TypeKind.LongType);
builder.lstore(durationSlot);
builder.invokestatic(ClassDesc.of(Profiler.class.getName()), "getProfilerStatistics", MethodTypeDesc.of(ClassDesc.of(ProfilerStatistics.class.getName())));
builder.ldc(statisticsId);
builder.invokevirtual(ClassDesc.of(ProfilerStatistics.class.getName()), "getMethodStatisticsById", MethodTypeDesc.of(ClassDesc.of(MethodStatistics.class.getName()), ConstantDescs.CD_int));
builder.lload(durationSlot);
builder.invokevirtual(ClassDesc.of(MethodStatistics.class.getName()), "trackRuntime", MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_long));
}
}
builder.with(element);
}
@Override
public void atEnd(CodeBuilder builder) {
}
@Override
public void atStart(CodeBuilder builder) {
System.out.println("AT START METHOD CODE TRANSFORM");
if(metricValues.contains(ProfilerMethodMetric.DEBUG.toString())) {
builder.getstatic(ClassDesc.of(System.class.getName()), "out", ClassDesc.of(PrintStream.class.getName()));
builder.ldc("DEBUG: This method is successfully injected");
builder.invokevirtual(ClassDesc.of(PrintStream.class.getName()), "println", MethodTypeDesc.of(ConstantDescs.CD_void, ClassDesc.of(String.class.getName())));
}
if(metricValues.contains(ProfilerMethodMetric.CALLS.toString())) {
builder.invokestatic(ClassDesc.of(Profiler.class.getName()), "getProfilerStatistics", MethodTypeDesc.of(ClassDesc.of(ProfilerStatistics.class.getName())));
builder.ldc(statisticsId);
builder.invokevirtual(ClassDesc.of(ProfilerStatistics.class.getName()), "getMethodStatisticsById", MethodTypeDesc.of(ClassDesc.of(MethodStatistics.class.getName()), ConstantDescs.CD_int));
builder.invokevirtual(ClassDesc.of(MethodStatistics.class.getName()), "trackCall", MethodTypeDesc.of(ConstantDescs.CD_void));
}
if(metricValues.contains(ProfilerMethodMetric.RUNTIME.toString())) {
builder.invokestatic(ClassDesc.of(System.class.getName()), "nanoTime", MethodTypeDesc.of(ConstantDescs.CD_long));
startSlot = builder.allocateLocal(TypeKind.LongType);
builder.lstore(startSlot);
}
}
};
}
然后我有一个
ClassTransform
列表,其中添加了新的,如下所示:
transforms.add(ClassTransform.transformingMethodBodies(mm -> mm.equals(methodModel), codeTransform));
以下是我尝试将它们组合起来的两种方法:
classfileBuffer
是包含原始类的 byte[]
,然后将其交给 Instrumentation API 以应用更改。
ClassTransform combined = null;
for(ClassTransform t : transforms) {
if(combined == null) {
combined = t;
} else {
combined = combined.andThen(t);
}
}
if(combined != null) {
classfileBuffer = ClassFile.of().transform(classModel, combined);
}
第一种方法显示我尝试使用
andThen()
,根据文档,它应该将两个类转换链接在一起。但这样一来,最后什么也没有注入,出于某种原因丢弃了所有转换。
for(ClassTransform t : transforms) {
classfileBuffer = ClassFile.of().transform(classModel, t);
classModel = ClassFile.of().parse(classfileBuffer);
}
这是我的第二种方法。我只是用第一次转换来转换 classModel,然后从缓冲区中解析一个新的 classModel,并一遍又一遍地重复这个过程。这对我来说最有意义,但这就是难以解释和理解的部分。
正如您在顶部的代码中看到的,我将此调试打印放在我的
atStart()
中,它在方法的每个代码转换开始时调用。虽然最后我的变换列表中有 2 个 ClassTransform
,但实际上只应用了第一个。无论我有多少个转换,打印语句都只会执行一次,如下所示:https://i.imgur.com/1QsfmfB.png
但我不明白为什么它再也没有被调用过。我还尝试对缓冲区的内容进行哈希处理,此循环仅更改它们一次,并且每次进一步的转换都被完全忽略,因为转换从未被调用。
这是一个最小的可重现示例,仍然显示相同的行为:
package at.alexkiefer.rcgj.transformers;
import at.alexkiefer.rcgj.Test;
import java.io.PrintStream;
import java.lang.classfile.*;
import java.lang.constant.ClassDesc;
import java.lang.constant.ConstantDescs;
import java.lang.constant.MethodTypeDesc;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MRE implements BytecodeTransformer {
private static CodeTransform generateMethodCodeTransform() {
return new CodeTransform() {
@Override
public void accept(CodeBuilder builder, CodeElement element) {
builder.with(element);
}
@Override
public void atEnd(CodeBuilder builder) {
}
@Override
public void atStart(CodeBuilder builder) {
System.out.println("\t\tAt start of methode code transform");
builder.getstatic(ClassDesc.of(System.class.getName()), "out", ClassDesc.of(PrintStream.class.getName()));
builder.ldc("DEBUG: This method is successfully injected");
builder.invokevirtual(ClassDesc.of(PrintStream.class.getName()), "println", MethodTypeDesc.of(ConstantDescs.CD_void, ClassDesc.of(String.class.getName())));
}
};
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassModel classModel = ClassFile.of().parse(classfileBuffer);
List<ClassTransform> transforms = new ArrayList<>();
for(ClassElement e : classModel.elements()) {
if(e instanceof MethodModel methodModel) {
if(classBeingRedefined.equals(Test.class)) {
System.out.println("Generating class transform for method " + methodModel.methodName());
CodeTransform codeTransform = generateMethodCodeTransform();
transforms.add(ClassTransform.transformingMethodBodies(mm -> mm.equals(methodModel), codeTransform));
}
}
}
int counter = 1;
for(ClassTransform t : transforms) {
System.out.println("Transform " + counter++ + " of " + transforms.size() + " is happening now");
System.out.println("\tClassfile buffer hash before transformation: " + Arrays.hashCode(classfileBuffer));
classfileBuffer = ClassFile.of().transform(classModel, t);
classModel = ClassFile.of().parse(classfileBuffer);
System.out.println("\tClassfile buffer hash after transformation: " + Arrays.hashCode(classfileBuffer));
}
return classfileBuffer;
}
}
public class Main {
public static void main(String[] args) {
Test test = new Test();
test.doSomething();
Profiler.inject();
test.doSomething();
test.doSomethingElse();
Profiler.eject();
//System.out.println(Profiler.getProfilerStatistics());
}
}
这是输出:
Generating class transform for method doSomething
Generating class transform for method doSomethingElse
Generating class transform for method <init>
Transform 1 of 3 is happening now
Classfile buffer hash before transformation: 1402060343
At start of methode code transform
Classfile buffer hash after transformation: -1078378321
Transform 2 of 3 is happening now
Classfile buffer hash before transformation: -1078378321
Classfile buffer hash after transformation: -1078378321
Transform 3 of 3 is happening now
Classfile buffer hash before transformation: -1078378321
Classfile buffer hash after transformation: -1078378321
DEBUG: This method is successfully injected
测试方法
doSomething
和 doSomethingElse
实际上不执行任何操作,只是计算一些不产生输出的内容的循环。第一个方法的调试打印已成功注入,在本例中为doSomething
(可以验证该函数在注入探查器之前调用一次并且不输出任何内容)。正如您所看到的,该列表包含三个类转换,第一个执行某些操作,其他两个则不执行某些操作。类文件没有被转换,这可以通过查看字节的哈希值来看出。此外,代码转换中的打印仅被调用一次。
另外:此类实现的接口
BytecodeTransformer
只是来自仪器 API 的 transform
的 ClassFileTransformer
方法的包装,这在这种情况下没有任何意义,但我在其他地方有需要。但这不应该是这里的问题。
问题在于谓词
mm -> mm.equals(methodModel)
。
MethodModel
没有定义相等性,因此它使用对象标识。
应用第一个转换器后,您将创建一个新的
ClassModel
,它也将具有与之前捕获的对象不匹配的不同 MethodModel
实例。因此,对于所有后续变换操作,谓词的计算结果为 false
。
您需要一个基于
methodName()
和 methodType()
的谓词:
if(e instanceof MethodModel methodModel) {
if(classBeingRedefined.equals(Test.class)) {
Utf8Entry name = methodModel.methodName(), type = methodModel.methodType();
System.out.println("Generating class transform for method " + name);
CodeTransform codeTransform = generateMethodCodeTransform();
transforms.add(ClassTransform.transformingMethodBodies(
mm -> mm.methodName().equals(name) && mm.methodType().equals(type),
codeTransform));
}
}
作为旁注,由于
classBeingRedefined
在整个方法中不会改变,因此在方法开始时首先测试它会更有效,如果不匹配则不采取任何操作(这种情况在以下情况下会更常见)所有类型都会调用 ClassFileTransformer
)。
此外,在不更改任何内容的情况下,返回
null
的效率稍高一些,因为调用者检查 null
比检查是否更改了字节数组更快(即使您返回原始数组,您可能已经更改了某些内容,因此调用者必须检查)。