Interception on constructor causes ClassNotFoundException

社会主义新天地 提交于 2020-01-02 10:18:58

问题


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

回答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.



来源:https://stackoverflow.com/questions/34219180/interception-on-constructor-causes-classnotfoundexception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!