首页 > 解决方案 > 我可以在调用 this() / super() 之前和初始化任何最终字段之前在构造函数中插入指令吗?

问题描述

前言

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

情况

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

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

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

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

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

问题/问题

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

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

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


讨论锑的答案后更新

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

  1. 为了避免调用this.getClass(),我需要将模拟目标的类名直接硬编码到父链上的所有构造函数中。即,如果两个“全局模拟”目标类具有相同的父类,则以下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很好地解释了“未初始化”。另一个相关的答案可以在这里找到。


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

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

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

标签: javabytecodejava-bytecode-asmbyte-buddybytecode-manipulation

解决方案


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

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

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

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

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

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

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

除了 this.getClass() 从超类构造函数之外,还有其他方法可以实例化类型吗?

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


推荐阅读