我目前正在一个maven post编译任务中生成一些ASM代码。在Java6中引入了一个StackMapTable来表示堆栈上的数据类型,这在以后的版本中是必需的。所以我会自动确定堆栈上最具体的类。现在我遇到了一个问题,在我的虚拟机中,ThaiBuddhistDate和HijrahDate继承了ChronoLocalDateImpl,所以它会在StackMapTable中创建这种类型,这显然会在其他虚拟机(甚至可能是版本)中崩溃。因此,我想,也许我应该将计算更改为最小强制,这可能会导致(理论上)类和接口出现类似的问题。现在我正在尝试为我的问题找到解决方案,所以我必须弄清楚可能会出现哪些差异。
一个额外的类只能出现在继承层次结构中的任何地方吗?假设JavaDoc具有如下继承层次结构:
对象- Foo - Bar - FooBar
我可以在继承结构中的任何地方都有额外的类吗?
对象- Baz - Foo - Bar - FooBar
对象- Foo - Baz - Bar - FooBar
对象- Foo - Bar - Baz - FooBar
与接口类似:接口是否也可以从文档中未定义的其他接口继承,或者一个类是否可以有额外的独立接口或接口,这些接口或接口基于已定义的接口或甚至没有接口?
发布于 2018-03-14 01:14:52
您似乎正在使用COMPUTE_FRAMES选项,它将导致ASM库通过getCommonSuperClass合并通过可能的代码路径遇到的类型,类似于旧的验证器所做的事情,并且有点颠覆了堆栈映射表的概念。
正如您已经注意到的,ASM的getCommonSuperClass实现可能会返回一个实际上无法访问的类型,比如一个JRE内部基类,并忽略接口关系。更大的问题是,您无法使用此方法的不同实现来修复此问题,因为传递给此方法的信息不足以确定正确的类型。
正确的类型是随后需要的,当然,它也应该与通过所有可能的代码路径提供的内容兼容,这是验证器将/应该检查的内容。如果代码生成器的设计方式是生成有效代码,则指定后续所需的类型应足以创建有效的堆栈映射表条目,但传递给getCommonSuperClass的传入类型不足以告诉您所需的类型。
为了说明这个问题,请考虑下面的示例类
class Example {
public static CharSequence problematicMethod() {
return Math.random()>0.5? new StringBuilder("x"): new StringBuffer("y");
}
}下面的代码分析编译后的(例如,由javac编写的)类,以及当被告知从头开始重新计算堆栈映射帧时,ASM将默认生成的内容:
static void printFrame(int nLocal, Object[] local, int nStack, Object[] stack) {
StringBuilder sb = decode(new StringBuilder().append("Locals: "), local, nLocal);
System.out.println(decode(sb.append(", Stack: "), stack, nStack));
}
private static StringBuilder decode(StringBuilder sb, Object[] array, int num) {
if(num==0) return sb.append("[]");
sb.append('[');
for(int ix = 0; ix<num; ix++) {
Object o = array[ix];
if(o==Opcodes.UNINITIALIZED_THIS) sb.append("this <uninit>");
else if(o==Opcodes.INTEGER) sb.append("int");
else if(o==Opcodes.FLOAT) sb.append("float");
else if(o==Opcodes.DOUBLE) sb.append("double");
else if(o==Opcodes.LONG) sb.append("long");
else if(o==Opcodes.NULL) sb.append("null");
else if(o==Opcodes.TOP) sb.append("-");
else sb.append(Type.getObjectType(o.toString()).getClassName());
sb.append(",");
}
sb.setCharAt(sb.length()-1, ']');
return sb;
}
public static void main(String[] args) throws IOException {
final MethodVisitor printFramesMV = new MethodVisitor(Opcodes.ASM5) {
@Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
printFrame(nLocal, local, nStack, stack);
}
};
final ClassVisitor printFrames = new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return name.equals("problematicMethod")? printFramesMV: null;
}
};
ClassReader cr = new ClassReader(Example.class.getName());
System.out.println("##original");
cr.accept(printFrames, ClassReader.EXPAND_FRAMES);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cr.accept(cw, ClassReader.SKIP_FRAMES);
System.out.println("##from ASM");
new ClassReader(cw.toByteArray()).accept(printFrames, ClassReader.EXPAND_FRAMES);
}这将打印出来
##original
Locals: [], Stack: []
Locals: [], Stack: [java.lang.CharSequence]
##from ASM
Locals: [], Stack: []
Locals: [], Stack: [java.lang.AbstractStringBuilder]这显示了您在问题中解释的相同问题,ASM将生成一个引用特定于实现的类的框架。javac生成的代码引用了所需的类型,该类型与方法的返回类型兼容。您可以研究getCommonSuperClass中的StringBuilder和StringBuffer,发现它们都实现了CharSequence,但这还不足以理解CharSequence在这里是正确的类型,因为我们可以简单地将示例更改为
class Example {
public static Appendable problematicMethod() {
return Math.random()>0.5? new StringBuilder("x"): new StringBuffer("y");
}
}并获取
##original
Locals: [], Stack: []
Locals: [], Stack: [java.lang.Appendable]
##from ASM
Locals: [], Stack: []
Locals: [], Stack: [java.lang.AbstractStringBuilder]由于传入的类实现了这两个接口,因此您无法通过查看传入的StringBuilder和StringBuffer类型来确定是正确的合并类型是CharSequence还是Appendable。
要进一步评估这个问题,请查看
class Example {
public static Comparable problematicMethod() {
return Math.random()>0.5? BigInteger.valueOf(123): Double.valueOf(1.23);
}
}它会产生
##original
Locals: [], Stack: []
Locals: [], Stack: [java.lang.Comparable]
##from ASM
Locals: [], Stack: []
Locals: [], Stack: [java.lang.Number]在这里,ASM的结果是一个public类型,但是这个公共基类没有实现所需的Comparable,所以这段代码实际上是错误的。
对于所有使用ASM的COMPUTE_FRAMES选项的代码生成器来说,这是非常幸运的,HotSpot的验证器对接口类型有很大的容忍度,换句话说,当两种类型中至少有一种是接口时,它根本不验证赋值的正确性(包括方法调用的接收者)。
如果您希望生成的代码在严格执行验证器的工作(甚至对于接口)时仍然有效,那么您不应该使用该选项,而是自己开始生成堆栈映射框架,方法是不使用COMPUTE_FRAMES选项并发出正确的visitFrame调用(或者,如果您正在使用tree API,则插入适当的节点)。
这样做似乎有一种普遍的恐惧,但这并不复杂。如前所述,这基本上意味着你的代码生成器已经知道了什么。这实际上不是试图找到一个通用类型,而是指定你以后要使用什么,如果你的代码生成器是正确的,那么它已经是正确的了,但是如果不正确,ASM的计算也不能修复代码。
回到你的具体例子,在处理ThaiBuddhistDate和HijrahDate时,你已经知道在分支合并点(我想)之后,你会把它们当作ChronoLocalDate来处理,而ASM最终会变成一个特定于实现的非public类型,但是如果这个类型不存在,ASM就会使用java.lang.Object,因为它不考虑接口。如果ASM考虑接口,它必须在ChronoLocalDate和Serializable之间做出选择,两者都不是更具体。这种设计根本无法解决这个问题。
为了进一步说明“合并传入类型”和“将使用什么”之间的结果有多大不同,请看
class Example {
public static void problematicMethod() {
if(Math.random()>0.5) {
java.awt.ScrollPane b = new java.awt.ScrollPane();
}
else {
javax.swing.JTabbedPane t = new javax.swing.JTabbedPane();
}
}
}##original
Locals: [], Stack: []
Locals: [], Stack: []
##from ASM
Locals: [], Stack: []
Locals: [java.awt.Container], Stack: []在这里,ASM浪费了资源来找出深层类层次结构树中的公共基类,而只需声明“drop the variable”就足够了…
https://stackoverflow.com/questions/49222338
复制相似问题