Посетитель метода не работает в Java ASM visitLineNumber () - PullRequest
0 голосов
/ 18 ноября 2018

Я хочу добавить вызов метода в каждую строку определенного класса.Для этого я хочу использовать библиотеку ASM (на основе посетителей).

Неработающая часть означает, что код (вызов метода) не вставлен.

Мой (не рабочий) код вКласс MethodVisitor до сих пор выглядит следующим образом:

@Override
public void visitLineNumber(int line, Label start) {
  mv.visitMethodInsn(
      Opcodes.INVOKESTATIC,
      classpath,
      "visitLine",
      "()V",
      false);
  super.visitLineNumber(line, start);

Я попробовал другой метод MethodVisitor, и он работал просто так:

@Override
public void visitInsn(int opcode) {
  mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          classpath,
          "visitLine",
          "()V",
          false);
  super.visitInsn(opcode);
}

Мой вопрос: почему первоене работает, но второй?

edit: Больше контекста:

Я хочу вставить вызов метода visitLine () в каждую строку кода.Возможный пример класса:

public class Calculator {
  public int evaluate(final String pExpression) {
    int sum = 0;
    for (String summand : pExpression.split("\\+")) {
      sum += Integer.parseInt(summand);
    }
    return sum;
  }
}

становится:

public class Calculator {
  public int evaluate(final String pExpression) {
    OutputWriter.visitLine();
    int sum = 0;
    OutputWriter.visitLine();
    for (String summand : pExpression.split("\\+")) {
      OutputWriter.visitLine();
      sum += Integer.parseInt(summand);
    }
    OutputWriter.visitLine();
    return sum;
  }
}

У меня есть базовая настройка ClassReader, ClassWriter и ClassVisitor, например:

ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(pClassName);
ClassVisitor tcv = new TransformClassVisitor(cw);
cr.accept(tcv, 0);
return cw.toByteArray();

В MethodVisitor я только переопределяю этот метод:

@Override
  public void visitLineNumber(int line, Label start) {
    System.out.println(line);
    mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            classpath,
            "visitLine",
            "()V",
            false);
    super.visitLineNumber(line, start);
  }

Это распечатывает все строки посещенных классов, но вызов метода, который я хотел добавить, не был добавлен или, по крайней мере, не выполнен.

edit: Выяснили некоторые новые вещи:

Вставка visitLineNumber работает, если она не вставляет что-либо в последнюю строку метода.

Например, класс калькулятора сверху:Пока в строке 7 (обратная строка) нет кода, код работает нормально.Я пробовал другой класс с 2 операторами возврата, и он также работал нормально, пока не достиг последнего оператора возврата.

Я думаю, что есть ошибка с порядком вставки вызова метода.Возможно, он вставляется после оператора return, что приводит к ошибке при проверке файла класса.

Есть новые идеи по теме?

1 Ответ

0 голосов
/ 22 ноября 2018

Здесь есть две проблемы.

Во-первых, кажется, что когда вызывается Instrumentation.retransformClasses, ошибки с преобразованным кодом, такие как VerifyError, не сообщаются JVM, а вместо этогопросто перейдите к старому коду.

Я не вижу здесь способа изменить поведение JVM.Стоит создать дополнительную тестовую среду, в которой вы используете другой метод для активации кода, такой как преобразование во время загрузки или просто статическое преобразование скомпилированных классов, и пытаетесь загрузить их.Это может быть в дополнение к производственному коду, который использует тот же код преобразования с retransformClasses, когда эти тесты не показывают ошибок.

Кстати, когда вы реализуете ClassFileTransformer, вы должны передать *Массив 1011 *, полученный в качестве параметра для метода transform для конструктора ClassReader(byte[]) вместо использования конструктора ClassReader(String).


Во-вторых, расположение кода последнего сообщенного номера строки такжецель филиала.Помните, что разрывы строк не генерируют код, поэтому конец цикла совпадает с началом оператора return.

ASM сообщит о связанных артефактах в следующем порядке:

  • visitLabel с экземпляром Label, связанным с местоположением кода
  • visitLineNumber с новым номером строки и Label из предыдущего шага
  • visitFrame чтобы сообщить фрейм карты стека, связанный с этим местоположением кода (поскольку он является целью ветвления)

Вы вставляете новую инструкцию при вызове visitLineNumber, в результате чего цель ветвления становится до этой новой инструкции, поскольку вы делегировали visitLabel перед ней.Но вызов visitFrame делегируется после , когда новые инструкции были вставлены, следовательно, больше не связаны с целью перехода.Это приводит к VerifyError, поскольку для каждой цели ветвления обязательно иметь кадр стековой карты.

Простое, но дорогостоящее решение состоит в том, чтобы не использовать кадры стековой карты исходного класса, но позволить ASM пересчитать зановоих.Т.е.

public static byte[] getTransformed(byte[] originalCode) {
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    ClassReader cr = new ClassReader(originalCode);
    ClassVisitor tcv = new TransformClassVisitor(cw);
    cr.accept(tcv, ClassReader.SKIP_FRAMES);
    return cw.toByteArray();
}

Кстати, когда вы сохраняете большую часть исходного кода, но вставляете только некоторые новые операторы, полезно оптимизировать процесс путем передачи ClassReader в ClassWriter 's constructor :

public static byte[] getTransformed(byte[] originalCode) {
    ClassReader cr = new ClassReader(originalCode);
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor tcv = new TransformClassVisitor(cw);
    cr.accept(tcv, ClassReader.SKIP_FRAMES);
    return cw.toByteArray();
}

Более эффективное решение, которое не пересчитывает кадры стековой карты (поскольку исходные кадры все еще подходят для такого простого преобразования), не так просто с API ASM.Единственная идея, которую я имею до сих пор, это отложить вставку новой инструкции до тех пор, пока фрейм не будет посещен, если он есть.К сожалению, это подразумевает переопределение всех visit методов для инструкций:

Оставайтесь с

public static byte[] getTransformed(byte[] originalCode) {
    ClassReader cr = new ClassReader(originalCode);
    ClassWriter cw = new ClassWriter(cr, 0);
    ClassVisitor tcv = new TransformClassVisitor(cw);
    cr.accept(tcv, 0);
    return cw.toByteArray();
}

и используйте

static class Transformator extends MethodVisitor {
    int lastLineNumber;

    public Transformator(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }
    public void visitLineNumber(int line, Label start) {
        lastLineNumber = line;
        super.visitLineNumber(line, start);
    }
    private void checkLineNumber() {
        if(lastLineNumber > 0) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, classpath,"visitLine","()V", false);
            lastLineNumber = 0;
        }
    }
    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
        checkLineNumber();
        super.visitTryCatchBlock(start, end, handler, type);
    }
    public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
        checkLineNumber();
        super.visitMultiANewArrayInsn(descriptor, numDimensions);
    }
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        checkLineNumber();
        super.visitLookupSwitchInsn(dflt, keys, labels);
    }
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        checkLineNumber();
        super.visitTableSwitchInsn(min, max, dflt, labels);
    }
    public void visitIincInsn(int var, int increment) {
        checkLineNumber();
        super.visitIincInsn(var, increment);
    }
    public void visitLdcInsn(Object value) {
        checkLineNumber();
        super.visitLdcInsn(value);
    }
    public void visitJumpInsn(int opcode, Label label) {
        checkLineNumber();
        super.visitJumpInsn(opcode, label);
    }
    public void visitInvokeDynamicInsn(
        String name, String desc, Handle bsmHandle, Object... bsmArg) {
        checkLineNumber();
        super.visitInvokeDynamicInsn(name, desc, bsmHandle, bsmArg);
    }
    public void visitMethodInsn(
        int opcode, String owner, String name, String desc, boolean iface) {
        checkLineNumber();
        super.visitMethodInsn(opcode, owner, name, desc, iface);
    }
    public void visitFieldInsn(int opcode, String owner, String name,String descriptor) {
        checkLineNumber();
        super.visitFieldInsn(opcode, owner, name, descriptor);
    }
    public void visitTypeInsn(int opcode, String type) {
        checkLineNumber();
        super.visitTypeInsn(opcode, type);
    }
    public void visitVarInsn(int opcode, int var) {
        checkLineNumber();
        super.visitVarInsn(opcode, var);
    }
    public void visitIntInsn(int opcode, int operand) {
        checkLineNumber();
        super.visitIntInsn(opcode, operand);
    }
    public void visitInsn(int opcode) {
        checkLineNumber();
        super.visitInsn(opcode);
    }
}

К сожалению, модель посетителя ASM не имеет preVisitInstr() или что-то подобное.

Обратите внимание, что при таком дизайне также невозможно ошибочно вводить инструкции после последней инструкции метода, поскольку введенные инструкции всегда помещаются перед другой инструкцией.

...