views:

502

answers:

1

I have a problem I am not able to solve. Let's assume we have the following two classes and an inheritance relationship:

public class A {
}

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

I want to instrument additional code such that it looks as follows:

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

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

In order to achieve this goal, I based my implementation on the java.lang.instrument package, using an Agent with my own class file transformer. The mechanism is also referred to as dynamic bytecode instrumentation.

Piece of cake so far. Now, my test method does the following:

Code:

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

This does not work due to the following restriction in the instrumentation package: when calling new B(), the instrumentation starts with class B and ends up in a compilation error when loading the manipulated class as the super class A has no print() method yet! The question arises if and how I can trigger the instrumentation of class A before class B. The transform() method of my classfiletransformer should be invoked with class A explicitly! So I started reading and bumped into this:

The java.lang.instrument.ClassFileTransformer.transform()'s javadoc says:

The transformer will be called for every new class definition and every class redefinition. The request for a new class definition is made with ClassLoader.defineClass. The request for a class redefinition is made with Instrumentation.redefineClasses or its native equivalents.

The transform method comes along with a class loader instance, so I thought, why not calling the loadClass method (loadClass calls defineClass) myself with class A when the instrumentation of B has started. I expected that the instrument method will be called as a result but sadly this was not the case. Instead the class A was loaded without instrumentation. (The agent does not intercept the load process although it is supposed to)

Any ideas, how to solve this problem? Do you see a reason why it is not possible that an agent that manipulates some bytecode cannot manually load another class that is then hopefully also send through that/any agent?

Note that the following code works properly since A was loaded and instrumented before B is manipulated.

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

Thanks a lot!

+2  A: 

I did not see any issues when I transformed B before A on the Sun 1.6.0_15 and 1.5.0_17 JREs (I used ASM). I would double-check the transformation code by running it externally and inspecting the resultant classes (e.g. with javap). I'd also check your classpath configuration to ensure A isn't loaded before your agent for some reason (perhaps check in your premain with getAllLoadedClasses).


EDIT:

If you load class A in your agent like this:

Class.forName("A");

...then an exception is thrown:

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

This makes sense - A becomes a dependency of the agent and it would not make sense for the agent to instrument its own code. You'd get an infinite loop that resulted in a stack overflow. Therefore, A is not processed by the ClassFileTransformer.


For completeness, here is my test code that works without problem. As mentioned, it depends on the ASM library.

The agent:

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());
  }

}

Method injector for 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();
    }

  }

}

Method replacer for 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);
      }
    }
  }
}

Common base code:

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();
  }

}

For some visible results, I added a System.out.println('X'); to A.print().

When run on this code:

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

...it produces this output:

transform: MainInstrumented
transform: B
transform: A
X
McDowell
Thx for your answer.Let us view the issue from a different perspective. Assume that both classes, A and B, are empty. add a log at the beginning and the end of the transform method, so we can see which class is loaded by the agent and at what point in time.execute: new B()the result should be: that class B is loaded by the agent and afterwards class A.can you try now to load class A manually using the Classloader.loadClass() method in ur agent when B passes it?the outcome is: B was loaded through the agent, A was not!Right?Cheerschristoph
First of all, thanks a lot for the answer and the effort. I appreciate that.U are right! I used Javassist for any transformations. Javassist recompiles the changes. This results in the compilation failure mentioned above. ASM directly works on the bytecode and there is no need for such thing as recompilation.Concerning the loading of classes within an agent: I do not understand your answer. I use eclipse and if I add in my Agent:> // if class name is B> Class.forName("A");and I follow the execution in the debugger, no Exception is thrown and the agent is not entered
Again, you are right. The exception you mentioned is correct. Let's make things even simpler: both classes have no methods and are not intended to be instrumented at all. The only think the agent should do is: if class B passes the agent, Class.forName("A"); shall be invoked. This should trigger the proper order of class loading (A first, then B). Try this example. You will see that only B passes the agent! So the question arises why A does not pass the agent when called as part of an agent.
+1 for great effort
HerdplattenToni