【问题标题】:Generate Invokedynamic with Javassist使用 Javassist 生成 Invokedynamic
【发布时间】:2013-03-14 12:31:10
【问题描述】:

我想我正在尝试做一些相对简单的事情。以 doSomething(int) 方法的以下 Java 字节码为例:

public java.lang.String doSomething(int i);
0 iload_1 [i]
1 invokestatic MyHelper.doSomething(int) : Java.lang.String
4 areturn

这个字节码几乎只是将调用转发给一个静态助手。

我现在要做的是使用 Javassist 将 invokestatic 替换为 invokedynamic。我知道如何用 ASM 做到这一点,因此只是假设我出于纯粹的好奇想知道它是如何工作的。以下是我的一些问题:

1) 以下是否正确:我不能使用 javassist CtMethod.instrument() 或 CtMethod.insertAt() 方法,因为这些方法需要一个包含有效 Java 表达式的字符串,而我不能用 Java 语法编写一个 invokedynamic?

2) invokestatic 的参数的处理方式与 invokevirtual 或 invokestatic 的参数一样,对吧?我的意思是,你把参数放在invokedynamic之前的堆栈上,就像你为invokevirtual做的那样?

3) 是否有使用 Javassist 创建调用动态字节码的示例代码(或者你能想出一些)?

这是我目前所知道的:你可以创建一个字节码对象,它有一个方法 addInvokedynamic()。但这需要 BootstrapMethodsAttribute 中的 BootstrapMethod 索引。 BootstrapMethod 反过来期望常量池中的方法句柄信息的索引,该索引需要方法引用等。所以本质上你必须自己管理整个常量池条目。没关系,但我担心我做错了,以后会引入奇怪的问题。有没有更简单的方法来做到这一点(一个辅助方法左右)?我的代码大致看起来像这样(我并没有真正“重写”上面的 invokestatic 但是:

void transform(CtMethod ctmethod, String originalCallDescriptor) {

    MethodInfo mInfo = ctmethod.getMethodInfo();
    ConstPool pool = ctmethod.getDeclaringClass().getClassFile().getConstPool();

    /* add info about the static bootstrap method to the constant pool*/
    int mRefIdx = /*somehow create a method reference entry*/
    int mHandleIdx = constPool.addMethodHandleInfo(ConstPool.REF_invokeStatic, mRefIdx);

    /* create bootstrap methods attribute; there can only be one per class file! */
    BootstrapMethodsAttribute.BootstrapMethod[] bms = new BootstrapMethodsAttribute.BootstrapMethod[] {
        new BootstrapMethodsAttribute.BootstrapMethod(mHandleIdx, new int[] {})
    };
    BootstrapMethodsAttribute bmsAttribute = new BootstrapMethodsAttribute(constPool, bms);
    mInfo.addAttribute(bmsAttribute);

    //... and then later, finally
    Bytecode bc = new Bytecode(constPool);
    ... push parameters ...
    bc.addInvokedynamic(0 /*index in bms array*/, mInfo.getName(), originalCallDescriptor);

    //replace the original method body with the one containing invokedynamic
    mInfo.removeCodeAttribute();
    mInfo.setCodeAttribute(bc.toCodeAttribute());

}

非常感谢您的帮助和时间!

【问题讨论】:

  • 好的,所以我上面的代码似乎非常正确。您可以通过调用 constPool.addMethodrefInfo() 获得 mRefIndex,您需要一个索引来引用包含引导方法的类(通过 constPool.addClassInfo 获得)和引导方法的名称/类型信息(通过 constPool.addNameAndTypeInfo()。然后把你所有的方法描述符都弄好,瞧,你就在那里。但是,它仍然是非常低级的,我很想知道 Javassist 中是否有更好的方法(或者是否会有更好的方法) .

标签: java code-generation javassist invokedynamic


【解决方案1】:

我必须说,这是一个非常有趣的问题。如果我的回答有点长,我很抱歉,但我已尽力为您提供尽可能多(我认为是)有用的信息,以帮助您和其他遇到此问题的人。

问题 1

以下是否正确:我不能使用 javassist CtMethod.instrument() 或 CtMethod.insertAt() 方法,因为这些方法需要一个包含有效 Java 表达式的字符串,而我不能用 Java 语法编写一个 invokedynamic?

你是对的。

CtMethod.insertAt() 只能用于 Java 代码而不是字节码操作码。 另一方面,CtMethod.instrument() 允许您处理字节码,甚至可以使用ExprEditorCodeConverter 以非常有限的方式对其进行修改。但正如我所说,它们允许您更改的内容非常有限,并且对于您试图实现的目标,这两个修饰符都无法帮助您。

问题 2

invokestatic 的参数的处理方式与 invokevirtual 或 invokestatic 的参数一样,对吧?我的意思是,你把参数放在invokedynamic之前的堆栈上,就像你为invokevirtual做的那样?

我不知道我是否完全理解您在这里真正要问的问题(您在第一句话中重复了 invokestatic)。我认为您要问的问题(如果我错了,请纠正我)是,如果 invokedynamic 中的参数的处理方式与 invokevirtual 中的参数处理方式相同调用静态。使您能够简单地将 invokevirtualinvokestatic 切换为 invokedynamic。我会假设它是在回答这个问题时......

首先要注意的是,invokevirtualinvokestatic 在处理堆栈时本身是不同的。 Invokevirtual 除了像 invokestatic 那样将所需的参数压入堆栈之外,它还压入对象引用,以便可以链接方法调用。


旁注

您可能已经知道这一点,但我添加此附加信息以防万一其他人遇到此问题并想知道为什么 invokestaticinvokevirtual 处理方式不同堆栈。

  • invokestatic 操作码用于调用类中的静态方法,这意味着在编译时 JVM 确切知道如何进行方法调用链接。

  • 另一方面,invokedynamic 操作码用于对象实例的方法调用。由于在编译时无法知道将方法调用链接到哪里,因此只有在 JVM 知道正确的对象引用时才能在运行时链接。


如果对操作码的工作方式有疑问,我的建议是查看JVM specification 中有关JVM instruction set 的章节(链接适用于JVM 7,编写本文时的当前版本)。

我刚刚这样做是为了检查我们在这里讨论的 3 个操作码。

invokestaticinvokedynamic 两个操作码具有相同的操作数堆栈定义:

..., [arg1, [arg2 ...]] →

...

正如我之前所说,invokevirtual 有一个不同的操作数堆栈定义,即:

..., objectref, [arg1, [arg2 ...]] →

...

我在这里的第一个假设(我必须警告您,我还没有深入了解 invokedynamic 操作码)是您无法更改 invokevirtual invokedynamic 的方式非常简单,就像您使用 invokestatic 一样。我这样说是因为 invokedynamic 不期望堆栈中有任何对象引用。

为了更好地理解这种情况,我的建议是使用 java.lang.invoke 包在 Java 中编写一个示例,这将允许您创建使用 invokedynamic 操作码的 java 字节码。编译类后,使用命令javap -l -c -v -p检查生成的字节码。

问题 3

是否有使用 Javassist 创建调用动态字节码的示例代码(或者你能想出一些)?

我不知道。我也用谷歌搜索了一下(你可能也已经这样做了),但我什么也没找到。我认为您的帖子将给出 javassist 的第一个代码示例:)

还有一些注意事项

所以本质上你必须自己管理整个常量池条目。没关系,但我担心我做错了,以后会引入奇怪的问题

只要您使用ConstPool 类来管理您的常量池,javassist 就会为您处理一切而不会产生问题。

此外,如果您创建了一个损坏的容器池,最常见(很可能总是)会发生的情况是,当您尝试加载类或调用修改后的方法时,您将立即遇到 ClassFormatException 错误。我会说这是其中一种情况,不管它是否有效。

我想不出这样一种场景,可以隐藏某种奇怪的错误,等待那个令人讨厌的时刻在你不那么期望的时候困扰你(注意我说我想不到,并不真的意味着他们不存在)。我什至敢说,只要你可以加载类并调用它的方法而不让 JVM 崩溃,你就可以了。

有没有更简单的方法来做到这一点(辅助方法左右)?

我不这么认为。 Javassist 在字节码修改方面为您提供了很多帮助,但它是在您使用更高级别的 API 时(例如,编写 java 代码并注入该代码或移动/复制 CtMethods、Ctclasses 等)。当您使用必须处理所有字节码的低级 API 时,您几乎只能靠自己了。

我知道这可能不是您正在寻找的答案,但我希望我已经对这个主题有所了解。

【讨论】:

  • 非常感谢您的出色回答!这不是我所希望的(=一种简单的方法),但获得所有这些证实我的假设的背景信息绝对是件好事。在这种情况下,我上面的代码可能会帮助其他尝试相同的人,我假设 Javassist 会在某个时候提出一个更简单的解决方案。关于invokedynamic 和invokevirtual:invokevirtual 有一个额外的第一个参数(“this”)当然是100% 正确的。因此,可能必须确保方法描述符是正确的(如有必要,还可能添加“this”作为第一个参数)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-07
  • 1970-01-01
  • 2012-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多