【发布时间】: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 插入我的代码 beforethis()或super()使用方法访问者。或者是否会在加载或重新转换期间以某种方式验证检测类的字节码,然后因为字节码“非法”而被拒绝?我想在开始学习 ASM 之前先了解一下,因为我想避免将时间浪费在一个不可行的想法上。如果类包含最终实例字段,我也无法在构造函数中初始化所有这些字段之前输入
return。这可能发生在复杂构造函数的最后,该构造函数在实际初始化最后一个字段之前会执行很多副作用。所以这个问题与上一个问题相似:我可以在初始化任何字段(包括最终字段)之前使用 ASM 插入我的if(...) return;并生成一个我无法使用 javac 生成的有效类和加载或重新转换时不会被拒绝?
顺便说一句,如果相关的话,我们谈论的是 Java 8+,即在撰写本文时,Java 版本是 8 到 14。
如果对这个问题有任何不清楚的地方,请随时提出后续问题,以便我改进。
讨论后更新Antimony's answer
我认为这种方法可以工作并避免副作用,调用构造函数链但避免任何副作用并导致所有字段为空的新初始化实例(null、0、false):
-
为了避免调用
李>this.getClass(),我需要将模拟目标的类名直接硬编码到父链上的所有构造函数中。 IE。如果两个“全局模拟”目标类具有相同的父类,则以下多个if块将被编织到每个相应的父类中,每个硬编码的子类名称一个。 为了避免创建对象或调用方法的任何副作用,我需要自己调用一个超级构造函数,为每个参数使用 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