如何在新的 Java 22 ClassFile Preview API 中结合 ClassTransforms

问题描述 投票:0回答:1

这是一个关于非常新功能的非常具体的问题。背景信息:我正在撰写关于 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
方法的包装,这在这种情况下没有任何意义,但我在其他地方有需要。但这不应该是这里的问题。

java jvm bytecode profiler
1个回答
0
投票

问题在于谓词

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
比检查是否更改了字节数组更快(即使您返回原始数组,您可能已经更改了某些内容,因此调用者必须检查)。

© www.soinside.com 2019 - 2024. All rights reserved.