【问题标题】:Can I insert instructions in constructors before calling this() / super() and before initialising any final fields?我可以在调用 this() / super() 之前和初始化任何最终字段之前在构造函数中插入指令吗?
【发布时间】:2020-05-23 04:48:45
【问题描述】:

前言

我一直在尝试使用 ByteBuddy 和 ASM,但我仍然是 ASM 的初学者,介于 ByteBuddy 的初学者和高级之间。这个问题是关于 ByteBuddy 和一般的 JVM 字节码限制的。

情况

我的想法是通过检测构造函数来创建用于测试的全局模拟,以便在每个构造函数的开头插入如下指令:

if (GlobalMockRegistry.isMock(getClass()))
  return;

仅供参考,GlobalMockRegistry 基本上包装了Set<Class<?>>,如果该集合包含某个类,则isMock(Class<?>> clazz) 将返回true。这个概念的优点是我可以在运行时(取消)激活每个类的全局模拟,因为如果多个测试在同一个 JVM 进程中运行,一个测试可能需要某个全局模拟,而下一个可能不需要。

上面的if(...) return;指令想要实现的是,如果模拟处于活动状态,构造函数不应该做任何事情:

  • 没有this() 或super() 来电, → 更新:不可能
  • 没有字段初始化,→ 更新:可能
  • 没有其他副作用。 → 更新:可能,请参阅下面的更新

结果将是一个具有未初始化字段的对象,不会产生任何(可能是昂贵的)副作用,例如资源分配(数据库连接、文件创建等)。我为什么要那个?难道我不能只用 Objenesis 创建一个实例并感到高兴吗?如果我想要一个全局模拟,即我无法注入的模拟对象,则不是,因为它们是在我无法控制的方法或字段初始化器中的某个地方创建的。如果其实例字段未正确初始化,请不要担心此类对象上的方法调用会做什么。假设我也检测了返回存根结果的方法。我已经知道该怎么做,问题只是这个问题上下文中的构造函数。

疑问/问题

现在,如果我尝试在 Java 源代码中模拟所需的结果,我会遇到以下限制:

  • 我无法在this() 或super() 之前插入任何代码。我可以通过使用相同的if(...) return; 检测超类层次结构来缓解这种情况,但我想知道我是否可以在理论上使用 ASM 插入我的代码 before this() 或 super() 使用方法访问者。或者是否会在加载或重新转换期间以某种方式验证检测类的字节码,然后因为字节码“非法”而被拒绝?我想在开始学习 ASM 之前先了解一下,因为我想避免将时间浪费在一个不可行的想法上。

  • 如果类包含最终实例字段,我也无法在构造函数中初始化所有这些字段之前输入return。这可能发生在复杂构造函数的最后,该构造函数在实际初始化最后一个字段之前会执行很多副作用。所以这个问题与上一个问题相似:我可以在初始化任何字段(包括最终字段)之前使用 ASM 插入我的if(...) return; 并生成一个我无法使用 javac 生成的有效类和加载或重新转换时不会被拒绝?

顺便说一句,如果相关的话,我们谈论的是 Java 8+,即在撰写本文时,Java 版本是 8 到 14。

如果对这个问题有任何不清楚的地方,请随时提出后续问题,以便我改进。


讨论后更新Antimony's answer

我认为这种方法可以工作并避免副作用,调用构造函数链但避免任何副作用并导致所有字段为空的新初始化实例(null、0、false):

  1. 为了避免调用this.getClass(),我需要将模拟目标的类名直接硬编码到父链上的所有构造函数中。 IE。如果两个“全局模拟”目标类具有相同的父类,则以下多个 if 块将被编织到每个相应的父类中,每个硬编码的子类名称一个。

    李>
  2. 为了避免创建对象或调用方法的任何副作用,我需要自己调用一个超级构造函数,为每个参数使用 null/zero/false 值。这无关紧要,因为链上的下一个父类将具有类似的代码块,因此给出的参数无论如何都无关紧要。

// Avoid accessing 'this.getClass()'
if (GlobalMockRegistry.isMock(Sub.class)) {
  // Identify and call any parent class constructor, ideally a default constructor.
  // If none exists, call another one using default values like null, 0, false.
  // In the class derived from Object, just call 'Object.<init>'.
  super(null, 0, false);
  return;
}

// Here follows the original byte code, i.e. the normal super/this call and
// everything else the original constructor does.

对我自己的注意:锑的回答很好地解释了“未初始化的this”。另一个相关答案可以在here找到。


评估我的新想法后的下一次更新

我设法通过概念验证验证了我的新想法。由于我的 JVM 字节码知识太有限,而且我不习惯它所需的思维方式(堆栈帧、局部变量表、首先推送/弹出变量的“反向”逻辑,然后对它们应用操作,无法轻松调试),我只是在 Javassist 而不是 ASM 中实现了它,相比之下,在经过数小时的试验和错误之后,使用 ASM 惨遭失败后,这是轻而易举的事。

我可以从这里开始,我要感谢用户 Antimony 的非常有启发性的回答 + cmets。我确实知道理论上可以使用 ASM 实现相同的解决方案,但相比之下它会非常困难,因为它的 API 对于手头的任务来说太低级了。 ByteBuddy 的 API 太高级了,Javassist 正好适合我,以便在这种情况下获得快速的结果(以及易于维护的 Java 代码)。

【问题讨论】:

  • 你可以在字节码级别(这是他们试图用 ASM 做的)。无论如何,构造函数要么必须调用父构造函数,要么抛出异常,没有异常 - 否则验证器将拒绝该类。
  • 嗯,我刚刚在一个带有手动插入语句的 Java 类上尝试了 ASMifier,然后在手动移动 visit* 指令后重新生成了运行生成的 ASM 代码的字节码。似乎我可以在初始化任何最终字段之前将 if 块移动到,这很好。但是如果我将它移到代表this() 或super() 的操作码上方,Java 运行时会抛出VerifyErrors。

标签: java bytecode java-bytecode-asm byte-buddy bytecode-manipulation


【解决方案1】:

是的,不是的。在这方面,Java 字节码的限制比 Java(源代码)少得多。只要您实际上不访问未初始化的对象,您就可以在构造函数调用之前放置您想要的任何字节码。 (对未初始化的this 值允许的唯一操作是调用构造函数,设置在同一类中声明的私有字段,并将其与null 进行比较)。

字节码在调用构造函数的位置和方式方面也更加灵活。例如,您可以在 if 语句中调用两个不同的构造函数之一,或者可以将超级构造函数调用包装在“try 块”中,这在 Java 语言级别上都是不可能的。

除了不访问未初始化的this 值之外,唯一的限制* 是对象必须沿着从构造函数调用返回的任何路径进行明确初始化。这意味着避免初始化对象的唯一方法是抛出异常。虽然比 Java 本身要宽松得多,但 Java 字节码的规则仍然是经过刻意构建的,因此不可能观察到未初始化的对象。一般来说,Java 字节码仍然需要内存安全和类型安全,只是类型系统比 Java 本身松散得多。从历史上看,Java 小程序被设计为在 JVM 中运行不受信任的代码,因此任何绕过这些限制的方法都是安全漏洞。

* 上面说的是传统的字节码验证,这是我最熟悉的。我相信 stackmap 验证的行为类似,但在某些 Java 版本中会出现实现错误。

附:从技术上讲,Java 可以在构造函数调用之前执行代码。如果将参数传递给构造函数,则首先评估这些表达式,因此为了编译 Java 代码,需要在构造函数调用之前放置字节码。同样,设置在同一类中声明的私有字段的能力用于设置嵌套类编译产生的合成变量。

如果类包含最终实例字段,我也无法在构造函数中初始化所有这些字段之前输入返回值。

然而,这是非常可能的。唯一的限制是您在未初始化的this 值上调用一些构造函数或超构造函数。 (由于所有构造函数递归都有这个限制,这最终会导致java.lang.Object的构造函数被调用)。但是,JVM 并不关心之后会发生什么。特别是,它只关心字段是否有一些类型良好的值,即使它是默认值(null 用于对象,0 用于 ints 等)因此无需执行字段初始化程序来给出它们是一个有意义的值。

除了 this.getClass() 从超类构造函数中获取要实例化的类型,还有其他方法吗?

据我所知没有。没有特殊的操作码可以神奇地获取与给定值关联的类。 Foo.class 只是由 Java 编译器处理的语法糖。

【讨论】:

  • 谢谢。如果您在我的问题下阅读我的第一条评论:(1)即使对于最终字段,似乎也可以抑制字段初始化。正确的?至少我的第一个实验表明了这一点。 (2) 我在尝试将if 块移动到this() 或super() 之前得到VerifyError 的原因是,如果我正确理解您的答案,不是我在那里执行任何指令,而是简单的事实我的if 条件使用getClass(),即隐含(尚未初始化)this,对吗? (待续)
  • (续) 除了this.getClass() 从超类构造函数中获取要实例化的类型,还有其他方法吗?我可以将SubType.class 生成到SubType 构造函数中,这不是问题。但是我也需要知道从超类型构造函数中创建了哪个子类型,以便访问我的全局模拟注册表的正确条目。
  • 我编辑了我的答案以解决您的问题。 1) 是 2) 不是
  • 一旦你调用了它的构造函数,它就被认为是初始化的。另请注意,在 init 方法中可能有多个自构造函数调用,尽管 Java 源代码不能这样做,因此理论上您可能需要使用您的工具来处理它。
  • 妈的,我怕你这么说。此外,即使我能做到这一点,这种方法也无法兑现避免副作用发生的承诺。如果在构造函数内部有类似super(new ComplexAndExpensive()) 的东西,即它自己创建参数而不只是传递接收到的参数,则可能存在很大的“副作用泄漏”。我开始理解为什么 PowerMock 通过检测用户选择的调用类而不是转换被调用者本身来进行构造函数拦截。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-01
  • 2021-04-01
  • 1970-01-01
  • 2012-06-30
相关资源
最近更新 更多