4

Java asm - How can I create a clone of a class with only the class name changed ?

I know that there's a simple way to modify the class name using asm SimpleRemapper, but I just want the outer class name changed without modifying the class names used in the methods. (please see below example)

Basically if I have a target class

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

I just wanted to create a clone that looks as:

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

(Note that the return type of clone and arg type of compare hasn't changed. This is intentional for my use case).

Kevin JJ
  • 333
  • 1
  • 2
  • 9
  • 1
    Seems like "java dynamic proxies" is more suitable for your use case. Did you tried to use it? – Link182 Aug 09 '20 at 15:28
  • @Link182 unfortunately, java dynamic proxies doesn't exactly fit in my need. I need this cloned class to have the implementation of certain methods, since there is a group of methods where this cloned class shouldn't relay to the original class. – Kevin JJ Aug 10 '20 at 02:11
  • seems like http://spoon.gforge.inria.fr/index.html is for you. – Link182 Aug 10 '20 at 08:44
  • 1
    It would be very easy actually, to achieve what you’ve described, literally. But I doubt that it will solve whatever problem you actually want to solve, as the resulting class would be entirely broken. – Holger Aug 10 '20 at 11:41
  • @Holger could you please let me know how I could do this? I'm probably looking at wrong docs and libraries but I couldn't find an easy way to do this. – Kevin JJ Aug 10 '20 at 14:02
  • @Holger Also, regarding your concern that the resulting class would be entirely broken, could you please give an example? – Kevin JJ Aug 10 '20 at 14:05
  • Thanks @Link182 Will look into this library. If possible, I'm hoping to do it with just asm to avoid introducing a new lib to our project, so please let me know if there are such ways. – Kevin JJ Aug 10 '20 at 14:08
  • Alternatively you can use an "compile time annotation processor". I think is a more easy solution which will generate your class at compile time and will include it in your artifacts. Also this solution bring more runtime(but not compile time) performance. So if is enough to generate your class at compile time you can try it. Is a very comfortable solution for libraries, example projects: Lombok, Dagger2... – Link182 Aug 10 '20 at 14:34

1 Answers1

5

Cloning a class and changing the name and only the name, i.e. leave every other class reference as-is, is actually very easy with the 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();

When chaining a ClassReader with a ClassWriter, the ClassVisitor in the middle only needs to overwrite those methods corresponding to an artifact it wants to change. So, to change the name and nothing else, we only need to override the visit method for the class’ declaration and pass a different name to the super method.

By passing the class reader to the class writer’s constructor, we’re even denoting that only little changes will be made, enabling subsequent optimizations of the transform process, i.e. most of the constant pool, as well as the code of the methods, will just get copied here.


It’s worth considering the implications. On the bytecode level, constructors have the special name <init>, so they keep being constructors in the resulting class, regardless of its name. Trivial constructors calling a superclass constructor may continue to work in the resulting class.

When invoking instance methods on ClonedTarget objects, the this reference has the type ClonedTarget. This fundamental property does not need to be declared and thus, there is no declaration that needs adaptation in this regard.

Herein lies the problem. The original code assumes that this is of type Target and since nothing has been adapted, the copied code still wrongly assumes that this is of type Target, which can break in various ways.

Consider:

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

This looks like not being affected by the issue. The generated default constructor just calls super() and will continue to work. The compare method has an unused parameter type left as-is. And the clone() method instantiates Target (unchanged) and returns it, matching the return type Target (unchanged). Seems fine.

But what’s not visible here, the clone method overrides the method Object clone() inherited from java.lang.Object and therefore, a bridge method will be generated. This bridge method will have the declaration Object clone() and just delegate to the Target clone() method. The problem is that this delegation is an invocation on this and the assumed type of the invocation target is encoded within the invocation instruction. This will cause a VerifierError.

Generally, we can not simply tell apart which invocations are applied on this and which on an unchanged reference, like a parameter or field. It does not even need to have a definite answer. Consider:

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

Implicitly assuming that this has type Target, it can use this and a Target instance from another source interchangeably. We can not change the this type and keep the parameter type without rewriting the code.

Other issues arise with visibility. For the renamed class, the verifier will reject unchanged accesses to private members of the original class.

Besides failing with a VerifyError, problematic code may slip through and cause problems at a later time. Consider:

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

Since this duplicate() does not override a superclass method, there won’t be a bridge method and all unchanged uses of Target are correct from the verifier’s perspective.

But the clone() method of Object does not return an instance of Target but of the this’ class, ClonedTarget in the renamed clone. So this will fail with a ClassCastException, only when being executed.


This doesn’t preclude working use cases for a class with known content. But generally, it’s very fragile.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thank you very much. The case I care the most is the deserialization of Lambda, assuming that SerializedLambda has the correct containing classes and etc. – Kevin JJ Aug 11 '20 at 14:47
  • By the way, is there a way to easily modify references in certain methods and don't modify anything in some methods (for e.g, don't modify anything in static methods)? – Kevin JJ Aug 11 '20 at 15:19
  • 2
    Override `visitMethod` and decide whether you return a custom `MethodVisitor` that will apply changes or just the result of `super.visitMethod`. See [this answer](https://stackoverflow.com/a/25568283/2711488) for example which will only change a `void main(String[])` method. Checking the `access` argument for the `static` modifier instead, should be a no-brainer. – Holger Aug 11 '20 at 15:36
  • I see. Thanks @Holger. I've given more thought about this approach and potential issues you mentioned. unfortunately it seems like it won't always work even for my use case. – Kevin JJ Aug 11 '20 at 20:40
  • I'm looking for an alternative and asked a question on that https://stackoverflow.com/questions/63366029/java-bytecode-asm-best-way-to-deal-with-the-subclass-reference-in-a-class-that. If you could please take a look, that'd be great! Thank you. – Kevin JJ Aug 11 '20 at 20:41