我可以在调用 this() / super() 之前和初始化任何 final 字段之前在构造函数中插入指令吗?

Can I insert instructions in constructors before calling this() / super() and before initialising any final fields?

前言

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

情况

我有一个想法,即通过在每个构造函数的开头插入这样的指令,通过检测构造函数来创建用于测试的全局模拟:

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

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

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

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

问题/难题

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

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

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


讨论后更新

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

  1. 为了避免调用 this.getClass(),我需要 hard-code 模拟目标的 class 名称直接进入 parent 链上的所有构造函数. IE。如果两个 "global mock" 目标 classes 具有相同的 parent class(es),则以下多个 if 块将被编织到每个对应的 parent class, 每个 hard-coded child class 一个名字.

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

// 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.

自言自语:Antimony 的回答很好地解释了 "uninitialised this"。可以找到另一个相关答案 .


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

我设法通过概念验证验证了我的新想法。由于我的 JVM 字节码知识太有限而且我不习惯它需要的思维方式(堆栈框架、局部变量表、"reverse" 第一个 pushing/popping 变量的逻辑,然后对它们应用操作,无法轻松调试),我只是在 Javassist[=112= 中实现了它] 而不是 ASM,相比之下,在经过数小时的反复试验和错误后,使用 ASM 惨遭失败后,这简直是小菜一碟。

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

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

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

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

* 上面说的是传统的字节码校验,因为这是我最熟悉的。我相信 stackmap 验证的行为类似,但在 Java.

的某些版本中除外实施错误

P.S。从技术上讲,Java 可以在构造函数调用之前执行代码。如果您将参数传递给构造函数,这些表达式将首先求值,因此 需要 在构造函数调用之前放置字节码的能力,以便编译 Java 代码。同样,设置在同一 class 中声明的私有字段的能力用于设置由嵌套 classes.

的编译产生的合成变量

If the class contains final instance fields, I also cannot enter a return before all of those fields have been initialised in the constructor.

然而,这是完全可能的。唯一的限制是您可以在未初始化的 this 值上调用一些构造函数或超构造函数。 (由于所有的构造函数递归地都有这个限制,这最终会导致java.lang.Object的构造函数被调用)。然而,JVM 并不关心之后会发生什么。特别是,它只关心字段有一些类型正确的值,即使它是默认值(对象为 null,整数为 0,等等)所以不需要执行字段初始值设定项以赋予它们有意义的值。

Is there any other way to get the type to be instantiated other than this.getClass() from a super class constructor?

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