Динамическое инструментирование байт-кодом - PullRequest
6 голосов
/ 06 августа 2009

У меня есть проблема, которую я не могу решить. Давайте предположим, что у нас есть следующие два класса и отношения наследования:

public class A {
}

public class B extends A {
    public void foo() {}
}

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

public class A {
    public void print() { }
}

public class B extends A {
     public void foo() { print(); }
}

Для достижения этой цели я основал свою реализацию на пакете java.lang.instrument, используя Агент с моим собственным преобразователем файлов классов. Этот механизм также называется динамическим инструментарием байт-кода.

Кусок торта до сих пор. Теперь мой метод тестирования делает следующее:

Код:

B b = new B();
b.foo();

Это не работает из-за следующего ограничения в пакете инструментария: при вызове new B() инструментарий начинается с класса B и заканчивается ошибкой компиляции при загрузке манипулируемого класса, поскольку суперкласс A не имеет печати ( ) метода пока нет! Возникает вопрос, можно ли и как запустить инструментарий класса A перед классом B. Метод transform () моего classfiletransformer должен вызываться с классом A явно! Поэтому я начал читать и наткнулся на это:

Javadoc java.lang.instrument.ClassFileTransformer.transform() говорит:

Требуется трансформатор каждое новое определение класса и каждый переопределение класса. Запрос на новое определение класса сделано с ClassLoader.defineClass. Запрос для переопределения класса производится с Instrumentation.redefineClasses или его нативные эквиваленты.

Метод transform поставляется вместе с экземпляром загрузчика классов, поэтому я подумал, почему бы не вызвать сам метод loadClass (loadClass вызывает defineClass) с классом A, когда началось инструментирование B. Я ожидал, что инструментальный метод будет вызван в результате, но, к сожалению, это был не тот случай. Вместо этого класс A был загружен без инструментов. (Агент не перехватывает процесс загрузки, хотя он должен это сделать)

Есть идеи, как решить эту проблему? Видите ли вы причину, по которой агент, который манипулирует каким-либо байт-кодом, не может вручную загрузить другой класс, который, как мы надеемся, также будет отправлен через этот / любой агент?

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

A a =  new A();
B b = new B();
b.foo();

Спасибо большое!

1 Ответ

8 голосов
/ 06 августа 2009

Я не видел никаких проблем, когда я преобразовал B до A на JRE Sun 1.6.0 & lt; x5f; 15 и 1.5.0 & # x5f; 17 (я использовал ASM ). Я бы дважды проверил код преобразования, запустив его извне и проверив получающиеся классы (например, с помощью javap). Я также проверил бы конфигурацию вашего classpath, чтобы убедиться, что A по какой-то причине не загружен перед вашим агентом (возможно, проверьте ваш premain с помощью getAllLoadedClasses ).


EDIT:

Если вы загружаете класс A в своего агента следующим образом:

Class.forName("A");

... затем выдается исключение:

Exception in thread "main" java.lang.NoSuchMethodError: B.print()V

Это имеет смысл - A становится зависимостью от агента, и агенту не имеет смысла использовать собственный код. Вы получите бесконечный цикл, который приведет к переполнению стека. Следовательно, A не обрабатывается ClassFileTransformer.

.

Для полноты вот мой тестовый код, который работает без проблем. Как уже упоминалось, это зависит от библиотеки ASM.

Агент:

public class ClassModifierAgent implements ClassFileTransformer {

  public byte[] transform(ClassLoader loader, String className,
      Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
      byte[] classfileBuffer) throws IllegalClassFormatException {
    System.out.println("transform: " + className);
    if ("A".equals(className)) {
      return new AModifier().modify(classfileBuffer);
    }
    if ("B".equals(className)) {
      return new BModifier().modify(classfileBuffer);
    }
    return classfileBuffer;
  }

  /** Agent "main" equivalent */
  public static void premain(String agentArguments,
      Instrumentation instrumentation) {
    instrumentation.addTransformer(new ClassModifierAgent());
  }

}

Метод инжектора для A:

public class AModifier extends Modifier {

  @Override
  protected ClassVisitor createVisitor(ClassVisitor cv) {
    return new AVisitor(cv);
  }

  private static class AVisitor extends ClassAdapter {

    public AVisitor(ClassVisitor cv) { super(cv); }

    @Override
    public void visitEnd() {
      MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "print", "()V",
          null, null);
      mv.visitCode();
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
          "Ljava/io/PrintStream;");
      mv.visitLdcInsn("X");
      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
          "println", "(Ljava/lang/String;)V");
      mv.visitInsn(Opcodes.RETURN);
      mv.visitMaxs(2, 1);
      mv.visitEnd();

      super.visitEnd();
    }

  }

}

Метод заменителя B:

public class BModifier extends Modifier {

  @Override
  protected ClassVisitor createVisitor(ClassVisitor cv) {
    return new BVisitor(cv);
  }

  class BVisitor extends ClassAdapter {

    public BVisitor(ClassVisitor cv) { super(cv); }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {
      if ("foo".equals(name)) {
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "()V",
            null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "B", "print", "()V");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
        return new EmptyVisitor();
      } else {
        return super.visitMethod(access, name, desc, signature, exceptions);
      }
    }
  }
}

Общий базовый код:

public abstract class Modifier {

  protected abstract ClassVisitor createVisitor(ClassVisitor cv);

  public byte[] modify(byte[] data) {
    ClassReader reader = new ClassReader(data);
    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor visitor = writer;
    visitor = new CheckClassAdapter(visitor);
    visitor = createVisitor(visitor);
    reader.accept(visitor, 0);
    return writer.toByteArray();
  }

}

Для некоторых видимых результатов я добавил System.out.println('X'); к A.print().

При запуске по этому коду:

public class MainInstrumented {
  public static void main(String[] args) {
    new B().foo();
  }
}

... это выдает:

transform: MainInstrumented
transform: B
transform: A
X
...