【问题标题】:Java bytecode asm - How can I create a clone of a class with only the class name changed?Java 字节码 asm - 如何创建仅更改类名的类的克隆?
【发布时间】:2020-08-08 19:53:48
【问题描述】:

Java asm - 如何创建只更改类名的类的克隆?

我知道有一种简单的方法可以使用 asm SimpleRemapper 修改类名,但我只想更改外部类名而不修改方法中使用的类名。 (请看下面的例子)

如果我有一个目标类

public class Target {
  public Target clone(...) ...
  public int compare(another: Target) ...
}

我只是想创建一个看起来像这样的克隆:

public class ClonedTarget {
  public Target clone(...) ...
  public int compare(another: Target) ...
}

(请注意,clone 的返回类型和 compare 的 arg 类型没有改变。这是我的用例有意设计的)。

【问题讨论】:

  • 似乎“java 动态代理”更适合您的用例。你试过用吗?
  • @Link182 不幸的是,java 动态代理并不完全符合我的需要。我需要这个克隆的类来实现某些方法,因为有一组方法,这个克隆的类不应该中继到原始类。
  • 实际上很容易实现你所描述的,字面意思。但我怀疑它是否能解决你真正想要解决的任何问题,因为生成的类将完全被破坏。
  • @Holger 你能告诉我我该怎么做吗?我可能正在查看错误的文档和库,但我找不到一个简单的方法来做到这一点。

标签: java java-8 bytecode java-bytecode-asm bytecode-manipulation


【解决方案1】:

使用 ASM API 克隆一个类并仅更改名称和名称,即保留所有其他类引用原样,实际上非常容易。

ClassReader cr = new ClassReader(Target.class.getResourceAsStream("Target.class"));
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        super.visit(version, access, "ClonedTarget", signature, superName, interfaces);
    }
}, 0);
byte[] code = cw.toByteArray();

当链接ClassReaderClassWriter 时,中间的ClassVisitor 只需要覆盖与它想要更改的工件相对应的那些方法。因此,要更改名称而不做其他任何事情,我们只需要重写类声明的 visit 方法,并将不同的名称传递给 super 方法。

通过将类读取器传递给类编写器的构造函数,我们甚至表示将只进行少量更改,从而可以对转换过程进行后续优化,即大部分常量池以及方法的代码, 只会被复制到这里。


值得考虑的影响。在字节码级别,构造函数具有特殊的名称<init>,因此无论其名称如何,它们一直是生成类中的构造函数。调用超类构造函数的普通构造函数可能会继续在生成的类中工作。

ClonedTarget 对象上调用实例方法时,this 引用的类型为ClonedTarget。这个基本属性不需要声明,因此在这方面没有需要修改的声明。

问题就在这里。原始代码假定this 的类型为Target,由于没有进行任何修改,因此复制的代码仍然错误地假定this 的类型为Target,这可能会以各种方式中断。

考虑:

public class Target {
  public Target clone() { return new Target(); }
  public int compare(Target t) { return 0;}
}

这似乎不受该问题的影响。生成的默认构造函数只调用super() 并将继续工作。 compare 方法保留了一个未使用的参数类型。而clone() 方法实例化Target(未更改)并返回它,匹配返回类型Target(未更改)。看起来不错。

但是这里看不到的是,clone 方法覆盖了从java.lang.Object 继承的方法Object clone(),因此会生成一个桥接方法。此桥接方法将具有声明 Object clone() 并仅委托给 Target clone() 方法。问题是这个委托是对this 的调用,调用目标的假定类型在调用指令中编码。这将导致VerifierError

通常,我们不能简单地区分哪些调用应用于this,哪些应用于未更改的引用,如参数或字段。它甚至不需要有明确的答案。考虑:

public void method(Target t, boolean b) {
    (b? this: t).otherMethod();
}

隐式假设this 具有Target 类型,它可以交替使用来自另一个来源的thisTarget 实例。在不重写代码的情况下,我们无法改变this的类型并保留参数类型。

可见性会出现其他问题。对于重命名的类,验证器将拒绝对原始类的private 成员的未更改访问。

除了使用VerifyError 失败之外,有问题的代码可能会漏掉并在以后引起问题。考虑:

public class Target implements Cloneable {
    public Target duplicate() {
        try {
            return (Target)super.clone();
        } catch(CloneNotSupportedException ex) {
            throw new AssertionError();
        }
    }
}

由于这个duplicate() 没有覆盖超类方法,因此不会有桥接方法,并且从验证者的角度来看,Target 的所有未更改使用都是正确的。

但是Objectclone() 方法不返回Target 的实例,而是this' 类的实例,ClonedTarget 在重命名的克隆中。因此,只有在执行时才会出现ClassCastException 失败。


这并不排除具有已知内容的类的工作用例。但总的来说,它非常脆弱。

【讨论】:

  • 非常感谢。我最关心的情况是 Lambda 的反序列化,假设 SerializedLambda 具有正确的包含类等。
  • 顺便问一下,有没有一种方法可以轻松修改某些方法中的引用,并且不修改某些方法中的任何内容(例如,不要修改静态方法中的任何内容)?
  • 覆盖 visitMethod 并决定是返回一个自定义的 MethodVisitor 来应用更改,还是只返回 super.visitMethod 的结果。例如,参见this answer,它只会更改void main(String[]) 方法。检查 static 修饰符的 access 参数应该是不费吹灰之力。
  • 我明白了。谢谢@Holger。我对这种方法和您提到的潜在问题进行了更多思考。不幸的是,它似乎并不总是适用于我的用例。
  • 我正在寻找替代方案并就stackoverflow.com/questions/63366029/… 提出了一个问题。如果你能看一看,那就太好了!谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-07
  • 1970-01-01
  • 2011-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多