ASM getMergedType和getCommonSuperClass问题

时间:2017-04-01 08:39:30

标签: java spring-boot java-bytecode-asm

我使用ASM更新类堆栈映射,但是当asm getMergedType时,会发生以下异常:

  

了java.lang.RuntimeException:
  java.io.IOException:找不到IntefaceImplA的资源。

如果没有asm修改类方法,它确实可以正常工作 我已经定义了两个接口A和B:IntefaceImplAIntefaceImplB

我的环境源代码:

IntefaceA.java

public interface IntefaceA {
    void inteface();
}

IntefaceImplA.java

public class IntefaceImplA implements IntefaceA {
@Override
public void inteface() {

}
}

IntefaceImplB.java

public class IntefaceImplB implements IntefaceA {
@Override
public void inteface() {

}
}

Test.java

public class Test {
public IntefaceA getImpl(boolean b) {
    IntefaceA a = b ? new IntefaceImplA() : new IntefaceImplB();
    return a;
}

}

Main.java

public class Main {
public static void main(String args[]) {
....
if (a instance of Test) {
..
...
}
}
}

编译了一个运行器jar后,从jar中手动删除了IntefaceImplA.class和IntefaceA.class。为什么我想删除那些类文件,因为春天总是喜欢这样做。

运行器jar可以在没有ASM的情况下正常运行,但使用Asm会发生异常。因为asm想要IntefaceImplA和IntefaceImplB的getMergedType,但是我删除了IntefaceImplA。 在调查ASM ClassWriter源代码后,我在下面找到了代码:

protected String getCommonSuperClass(String type1, String type2) 
{
    ClassLoader classLoader = this.getClass().getClassLoader();
    Class c;
    Class d;
    try {
        c = Class.forName(type1.replace('/', '.'), false, classLoader);
        d = Class.forName(type2.replace('/', '.'), false, classLoader);
    } catch (Exception var7) {
        throw new RuntimeException(var7.toString());
    }

    if(c.isAssignableFrom(d)) {
        return type1;
    } else if(d.isAssignableFrom(c)) {
        return type2;
    } else if(!c.isInterface() && !d.isInterface()) {
        do {
            c = c.getSuperclass();
        } while(!c.isAssignableFrom(d));

        return c.getName().replace('.', '/');
    } else {
        return "java/lang/Object";
    }
}
实际上,我删除了相关的类文件,类加载器找不到类。但没有asm,程序确实正常工作 我应该增强对getCommonSuperClass方法的覆盖,如果发生异常,那么为它返回java / lang / Object吗?这很有趣

1 个答案:

答案 0 :(得分:4)

通常,覆盖getCommonSuperClass以使用其他策略,例如没有加载类,是一个有效的用例。正如it’s documentation所述:

  

此方法的默认实现加载两个给定的类,并使用java.lang.Class方法查找公共超类。可以覆盖它以其他方式计算这个常见的超类型,特别是在没有实际加载任何类的情况下,或者考虑当前由此ClassWriter生成的类,当然,由于它正在构建中,因此当然不能加载它

除了一个或两个参数都是你正在构建(或大幅改变)的类的可能性之外,可能的情况是代码转换工具的上下文不是最终运行类的上下文,所以它们是不必在该上下文中通过Class.forName访问。由于Class.forName使用调用者的上下文(ASM的ClassWriter),因此ASM甚至可能无法访问该类,尽管它在使用ASM的代码上下文中可用(如果不同的类加载器是参与)。

另一个有效的方案是通过使用已经可用的元信息来更有效地解决请求,而无需实际加载类。

但是,当然,返回"java/lang/Object"并不是一个有效的解决方案。虽然这确实是每个参数的常见超类型,但它不一定是代码的正确类型。为了坚持你的榜样,

public IntefaceA getImpl(boolean b) {
    IntefaceA a = b ? new IntefaceImplA() : new IntefaceImplB();
    return a;
}

公共超类型IntefaceImplAIntefaceImplB不仅需要验证为其分配任何类型的有效性,它还是条件表达式的结果类型,必须可以分配给它方法的返回类型。如果您使用java/lang/Object作为常见超类型,则验证程序将拒绝该代码,因为它无法分配给IntefaceA

原始的stackmap很可能会将IntefaceA报告为公共超级,它将被验证者接受,因为该类型与方法的返回类型相同,因此即使不加载类型,也可以认为它是可分配的。无论是IntefaceImplA还是IntefaceImplB,该测试都可以分配给指定的公共类型,可能会推迟到实际加载这些类型的位置,因为您说,您删除了IntefaceA ,这永远不会发生。

缺少声明的返回类型的方法根本无法工作。你的观察结果的唯一解释是“在没有程序正常工作的情况下”,就是在测试过程中从未调用过这种方法。你很可能通过删除正在使用的类来在你的软件中创建一个定时炸弹。

目前尚不清楚为什么要这样做。你的解释“因为春天总是喜欢做这些东西”远远不能理解。

但您可以使用重写方法获得与未修改代码相同的行为。返回java/lang/Object只是行不通。你可以用

@Override
protected String getCommonSuperClass(String type1, String type2) {
    if(type1.matches("IntefaceImpl[AB]") && type2.matches("IntefaceImpl[AB]"))
        return "IntefaceA";
    return super.getCommonSuperClass(type1, type2);
}

当然,如果您删除了更多的类文件,则必须添加更多特殊情况。

完全不同的方法是不使用COMPUTE_FRAMES选项。这个选项意味着ASM将从头开始重新计算所有堆栈映射帧,这对于懒惰的程序员来说非常有用,但是如果你只对现有类进行少量代码转换,则会产生许多不必要的工作,当然,这也要求有一个有效的getCommonSuperClass方法。

如果没有该选项,ClassWriter将只重现ClassReader报告的帧,因此所有未更改的方法也将保持不变的堆栈映射。您将不得不关心您更改其代码的方法,但是对于许多典型的代码转换任务,您仍然可以保留原始帧。例如。如果你只是将方法调用重定向到与签名兼容的目标或者注入使堆栈处于与它们之前相同状态的日志语句,你仍然可以保留原始帧,这是自动发生的。请注意ClassWriter(ClassReader,int) constructor的存在,它可以更有效地传输您不会更改的方法。

仅当您更改分支结构或使用分支插入代码时,您才需要关注帧。但即便如此,通常还是值得学习如何做到这一点,因为自动计算非常昂贵,而您在进行代码转换时通常已经掌握了必要的信息。