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 Посмотрю эту библиотеку. Если возможно, я надеюсь сделать это только на ассемблере, чтобы не вводить новую библиотеку в наш проект, поэтому, пожалуйста, дайте мне знать, есть ли такие способы.   -  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