Java bytecode asm - Как мога да създам клонинг на клас само с променено име на клас?

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 не са променени. Това е умишлено за моя случай на употреба).


person Kevin JJ    schedule 08.08.2020    source източник
comment
Изглежда, че динамичните проксита на Java са по-подходящи за вашия случай на употреба. Опитахте ли да го използвате?   -  person Link182    schedule 09.08.2020
comment
@Link182 за съжаление динамичните прокси сървъри на Java не отговарят точно на нуждите ми. Имам нужда от този клониран клас, за да има внедряване на определени методи, тъй като има група от методи, при които този клониран клас не трябва да се препредава към оригиналния клас.   -  person Kevin JJ    schedule 10.08.2020
comment
изглежда, че spoon.gforge.inria.fr/index.html е за вас.   -  person Link182    schedule 10.08.2020
comment
Всъщност би било много лесно да постигнете това, което описахте, буквално. Но се съмнявам, че това ще реши какъвто и да е проблем, който всъщност искате да разрешите, тъй като полученият клас ще бъде напълно счупен.   -  person Holger    schedule 10.08.2020
comment
@Holger бихте ли ми казали как мога да направя това? Вероятно разглеждам грешни документи и библиотеки, но не можах да намеря лесен начин да направя това.   -  person Kevin JJ    schedule 10.08.2020
comment
@Holger Също така, по отношение на вашата загриженост, че полученият клас ще бъде напълно счупен, можете ли да дадете пример?   -  person Kevin JJ    schedule 10.08.2020
comment
Благодаря @Link182 Ще разгледам тази библиотека. Ако е възможно, надявам се да го направя само с asm, за да избегна въвеждането на нов lib в нашия проект, така че, моля, уведомете ме, ако има такива начини.   -  person Kevin JJ    schedule 10.08.2020
comment
Като алтернатива можете да използвате процесор за анотации по време на компилиране. Мисля, че е по-лесно решение, което ще генерира вашия клас по време на компилиране и ще го включи във вашите артефакти. Също така това решение носи повече производителност по време на изпълнение (но не и по време на компилиране). Така че, ако е достатъчно, за да генерирате вашия клас по време на компилиране, можете да го опитате. Е много удобно решение за библиотеки, примерни проекти: Lombok, Dagger2...   -  person Link182    schedule 10.08.2020


Отговори (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();

Когато свързвате ClassReader с ClassWriter, 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 заменя метода Object clone(), наследен от java.lang.Object и следователно ще бъде генериран мостов метод. Този мостов метод ще има декларацията Object clone() и просто ще делегира на метода Target clone(). Проблемът е, че това делегиране е извикване на this и предполагаемият тип на целта за извикване е кодиран в инструкцията за извикване. Това ще доведе до VerifierError.

Като цяло не можем просто да различим кои извиквания се прилагат към this и кои към непроменена препратка, като параметър или поле. Дори не е необходимо да има категоричен отговор. Обмисли:

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

Имплицитно приемайки, че this има тип Target, той може да използва this и Target екземпляр от друг източник взаимозаменяемо. Не можем да променим типа this и да запазим типа на параметъра, без да пренапишем кода.

Други проблеми възникват с видимостта. За преименувания клас верификаторът ще отхвърли непроменен достъп до private члена на оригиналния клас.

Освен неуспех с VerifyError, проблемният код може да се промъкне и да причини проблеми по-късно. Обмисли:

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

Тъй като този duplicate() не отменя метод на суперклас, няма да има мостов метод и всички непроменени употреби на Target са правилни от гледна точка на проверяващия.

Но методът clone() на Object не връща екземпляр на Target, а на класа this’, ClonedTarget в преименувания клонинг. Така че това ще се провали с ClassCastException, само когато се изпълнява.


Това не изключва работни случаи на употреба за клас с известно съдържание. Но като цяло е много крехко.

person Holger    schedule 10.08.2020
comment
Благодаря ти много. Случаят, който ме интересува най-много, е десериализацията на Lambda, като се приеме, че SerializedLambda има правилните съдържащи класове и т.н. - person Kevin JJ; 11.08.2020
comment
Между другото, има ли начин лесно да модифицирате препратките в определени методи и да не модифицирате нищо в някои методи (напр. не модифицирайте нищо в статични методи)? - person Kevin JJ; 11.08.2020
comment
Заменете visitMethod и решете дали да върнете персонализиран MethodVisitor, който ще приложи промени или само резултата от super.visitMethod. Вижте този отговор например, който ще промени само void main(String[]) метод. Проверяването на аргумента access за модификатора static вместо това трябва да е безпроблемно. - person Holger; 11.08.2020
comment
Виждам. Благодаря @Holger. Обмислих повече този подход и потенциалните проблеми, които споменахте. за съжаление изглежда, че няма да работи винаги дори за моя случай на употреба. - person Kevin JJ; 11.08.2020
comment
Търся алтернатива и зададох въпрос относно това stackoverflow.com/questions/63366029/. Ако можете да погледнете, ще бъде страхотно! Благодаря ти. - person Kevin JJ; 11.08.2020