2

I'm trying to intercept constructors annotated with @Inject. That worked fine in the context of a small unit test. However in the context of a DI container like Spring it fails with a ClassNotFoundException.

I managed to narrow down on the root cause. Calling getDeclaredConstructors on the instrumented class will trigger this exception. Interestingly enough, if we first create an instance of that class, the problem disappears.

For example:

public class InterceptConstructorTest {

    @Test
    public void testConstructorInterception() throws ClassNotFoundException {

        ByteBuddyAgent.install();

        new AgentBuilder.Default().type(nameStartsWith("test")).transform(new AgentBuilder.Transformer() {

            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription td) {

                return builder.constructor(isAnnotatedWith(Inject.class))
                        .intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.to(ConstructorInterceptor.class)));
            }
        }).installOnByteBuddyAgent();

        // If this line is uncommented, ClassNotFoundException won't be thrown
//      MyClass myClass = new MyClass("a param");

        // Manually load MyClass
        Class<?> myClassDefinition = getClass().getClassLoader().loadClass("test.MyClass");

        // Throws NoClassDefFoundError
        for(Constructor<?> constructor : myClassDefinition.getDeclaredConstructors()) {
            System.out.println(constructor);
        }
    }
}

The stack stack trace can be found: http://pastebin.com/1zhx3fVX

class MyClass {

    @Inject
    public MyClass(String aParam) {
        System.out.println("constructor called");
    }
}

class ConstructorInterceptor {

    public static void intercept() {
        System.out.println("Intercepted");
    }
}
Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
user3408654
  • 301
  • 1
  • 13
  • 1
    Since Version 0.7.7, Byte Buddy's new default `InitiailizationStrategy` takes care of the problem for you. The new version is currently synchronized with the Maven Central Repository. – Rafael Winterhalter Dec 14 '15 at 07:55
  • After moving to 0.7.7 my application prematurely exits, before the `main` is executed. No exceptions were raised. Any idea of how I can help you figure out this issue? – user3408654 Dec 14 '15 at 10:19
  • That should not happen, even if the instrumentation is failing. You can always add a `AgentBuilder.Listener` to check if Byte Buddy issues an error. Maybe your agent is still using an older version of Byte Buddy causing a `NoClassDefFoundError`? Does your unit test work with the new version? If so, I assume that you have a versioning conflict somewhere. – Rafael Winterhalter Dec 14 '15 at 10:43
  • Did you find out what happened? – Rafael Winterhalter Dec 14 '15 at 15:42
  • Sorry for the delay. My test still passes with 0.7.7. However on a more complex program where I have `AgentBuilder.Default().type(any())`, it prematurely exits. There is no exceptions raised. And no errors reported by the `AgentBuilder.Listener`. It just died right after a transformation was applied to `sun.management.AgentConfigurationError`. I have to admit I went a little bit overboard using the `any` matcher. I changed it to not transform java/javax/sun classes and the problem goes away. Not sure if you'd like me to investigate further? (I just didn't know how to go from there) – user3408654 Dec 15 '15 at 12:41
  • And this does not happen with 0.7.6? If you instrument any class with any class loader, then the JVM might just crash. The instrumentation API is not bulletproof. Instead of the name-based restriction, it might also help to use `type(any(), not(isBootstrapClassLoader()))` - this way, no *internal classes* are ever instrumented. If you find out what really happens, I would of course appreciate feedback on that, especially if the behavior changed with 0.7.7, this should not be possible, in my eyes. – Rafael Winterhalter Dec 15 '15 at 12:43
  • Thanks for that matcher, I'll give it a try. Actually the crash does not happen in 0.7.6 WHEN I use `.withInitializationStrategy(InitializationStrategy.Premature.INSTANCE)` (I had added that based on your initial first comment). However if I take it out, it crashes at the exact same transformation. – user3408654 Dec 15 '15 at 13:06
  • Using the premature `InitializationStrategy` avoids the use of class initializers for certain types. I assume that such crucial change in its internal classes is to much for the JVM so it just collapses. – Rafael Winterhalter Dec 15 '15 at 13:25
  • I see, thanks for all the help. I'll be more careful with my matchers from now on... – user3408654 Dec 15 '15 at 13:32
  • It is mostly about the non-public `sun.*` types. The JVM sometimes hard-codes certain assumptions about classes into its implementation and cannot recover from changes. Ideally, such classes would not be instrumentable but that is how it is. – Rafael Winterhalter Dec 15 '15 at 13:40

1 Answers1

1

The problem in this case is the constructor injection. In order to rebase a constructor, Byte Buddy needs to create an additional type and creates a class like the following:

class MyClass {

    private synthetic MyClass(String aParam, $SomeType ignored) {
        System.out.println("constructor called");
    }

    @Inject
    public MyClass(String aParam) {
      this(aParam, null);
      // Instrumentation logic.
    }
}

The additional type is unfortunately necessary to create a unique signature for the rebased constructors. With methods, Byte Buddy can rather change the name but for constructors that is not possible as they must be named <init> in the class file to be recognized as constructors.

Byte Buddy tries to only load auxiliary classes after a type was instrumented. Depending on the virtual machine, loading a class that references another class causes the loading of the referenced type. If this type is the instrumented class, the instrumentation aborts the ongoing instrumentation for the circularity.

Therefore, Byte Buddy makes sure that any auxiliary type is only loaded at the first possible point after it can be sure that the instrumented type is loaded. And it does this by adding a self-initialization into the instrumented class's class initializer. In a way, Byte Buddy adds a block:

static {
  ByteBuddy.loadAuxiliaryTypes(MyClass.class);
}

If this block is not executed before reflecting on the class, the auxiliary type is not loaded and the exception you encounter is thrown. If you called:

Class.forName("test.MyClass", true, getClass().getClassLoader());

instead of loadClass, the problem would not occur where the second parameter indicates to execute the class initializer eagerly. Also, the initializer is executed if you create an instance.

Of course, this is not satisfactory, I am now adding some logic to decide for an auxiliary type if it can be loaded during the instrumentation to avoid such errors.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192