Почему этот простой фрагмент кода выводит поврежденный файл класса? - PullRequest
0 голосов
/ 15 октября 2018
ClassReader classReader = new ClassReader(new FileInputStream(new File("input.class")));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
Files.write(Paths.get("output.class"), classWriter.toByteArray());

При декомпиляции output.class Я получаю

package corrupted_class_files;

input.class - это хорошо, и я могу использовать ClassReader для чтения инструкций, я просто не могу сохранить класс

1 Ответ

0 голосов
/ 17 октября 2018

В вашем коде отсутствует шаг фактического копирования объектов класса из источника в место назначения:

try(FileInputStream in = new FileInputStream(new File("input.class")) {
    ClassReader classReader = new ClassReader(in);
    ClassWriter classWriter = new ClassWriter(classReader, 0);
    classReader.accept(classWriter, 0);
    Files.write(Paths.get("output.class"), classWriter.toByteArray());
}

Передача ClassReader в конструктор ClassWriter не копирует объекты, этоскорее включает оптимизацию, если ваше преобразование сохраняет большинство исходных файлов классов.Или, как указывает документация ClassWriter(ClassReader classReader, int flags):

Создает новый объект ClassWriter и позволяет оптимизировать «главным образом добавление» преобразований байт-кода.Эти оптимизации следующие:

  • Методы постоянного пула и начальной загрузки из исходного класса копируются, как и в новый класс, что экономит время.Новые записи постоянного пула и новые методы начальной загрузки будут добавлены в конце при необходимости, но неиспользуемые записи постоянного пула или методы начальной загрузки не будут удалены .
  • Методы, которые не преобразованы, копируютсякак и в новом классе, непосредственно из исходного байт-кода класса (т. е. без отправки событий посещения для всех инструкций метода), что экономит лот времени.Нетрансформированные методы обнаруживаются тем фактом, что ClassReader получает объекты MethodVisitor, которые поступают из ClassWriter (а не из любого другого экземпляра ClassVisitor).

Таким образом, когда вы соединяете ClassWriter напрямуюв ClassReader в методе accept все посетители метода будут происходить из модуля записи, следовательно, все они будут скопированы напрямую.

Когда вы собираетесь существенно изменить класс или создаете новый класс, вместо этого вы бы использовали конструктор ClassWriter(int flags).

Обратите внимание, что COMPUTE_FRAMES уже подразумевает COMPUTE_MAXS.В приведенном выше примере я не указал ни один, так как методы все равно копируются.Если вы действительно хотите изменить или добавить код и вам нужно COMPUTE_FRAMES, стоит указать SKIP_FRAMES для считывателя, поскольку нет смысла декодировать исходные кадры, когда они все равно пересчитываются с нуля.

Таким образом, типичная установка преобразования выглядит следующим образом:

public class MyClassVisitor extends ClassVisitor {

    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        MethodVisitor visitor = super.visitMethod(
            access, name, desc, signature, exceptions);
        if(method matches criteria) {
            visitor = new MyMethodVisitorAdapter(visitor);
        }
        return visitor;
    }
}
try(FileInputStream in = new FileInputStream(new File("input.class"))) {
    ClassReader classReader = new ClassReader(in);
    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
    classReader.accept(new MyClassVisitor(classWriter), ClassReader.SKIP_FRAMES);
    Files.write(Paths.get("output.class"), classWriter.toByteArray());
}

При связывании посетителей через конструкторы каждый метод, который вы не переопределяете, делегирует прикованному посетителю, реплицируя исходную конструкцию, когда завершаетсяцель ClassWriter, соответственноMethodVisitor предоставлено ClassWriter.Если метод не удовлетворяет вашему условию преобразования, поэтому вы возвращаете исходное значение MethodVisitor, описанная выше оптимизация по-прежнему применяется.Посетитель метода следует тому же шаблону, что и посетитель класса, переопределяя метод, который вы хотите перехватить.

Кстати, вам следует избегать смешивания старых операций ввода-вывода и NIO.Упрощенный вариант вашего кода выглядит следующим образом:

ClassReader classReader = new ClassReader(Files.readAllBytes(Paths.get("input.class")));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
classReader.accept(new MyClassVisitor(classWriter), ClassReader.SKIP_FRAMES);
Files.write(Paths.get("output.class"), classWriter.toByteArray());

Обратите внимание на симметрию между чтением и записью

Хотя при использовании getResource и др. Вы можетевынужден иметь дело с InputStream.Но для классов, доступных через системный загрузчик классов, вы также можете просто передать имя класса конструктору ClassReader(String).

...