拦截构造函数导致 ClassNotFoundException

Interception on constructor causes ClassNotFoundException

我正在尝试拦截用 @Inject 注释的构造函数。这在小型单元测试的上下文中运行良好。然而,在像 Spring 这样的 DI 容器的上下文中,它失败并显示 ClassNotFoundException.

我设法缩小了根本原因的范围。在检测的 class 上调用 getDeclaredConstructors 将触发此异常。有趣的是,如果我们首先创建那个 class 的实例,问题就会消失。

例如:

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

可以找到堆栈跟踪: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");
    }
}

本例中的问题是构造函数注入。为了 rebase 构造函数,Byte Buddy 需要创建一个额外的类型并创建一个 class,如下所示:

class MyClass {

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

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

不幸的是,附加类型对于为变基构造函数创建唯一签名是必需的。对于方法,Byte Buddy 可以更改名称,但对于构​​造函数来说这是不可能的,因为它们 必须 在 class 文件中被命名为 <init> 才能被识别为构造函数.

Byte Buddy 尝试仅加载辅助 classes 类型被检测之后。根据虚拟机,加载引用另一个 class 的 class 会导致加载引用的类型。如果此类型是已检测的 class,检测将中止正在进行的循环检测。

因此,Byte Buddy 确保任何辅助类型仅在可以确定加载检测类型后的第一个可能点加载。它通过将 自初始化 添加到检测 class 的 class 初始化器中来实现。在某种程度上,字节好友添加了一个区块:

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

如果在反射到class之前不执行此块,则不会加载辅助类型并抛出您遇到的异常。如果您致电:

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

而不是 loadClass,如果第二个参数指示立即执行 class 初始化程序,则不会出现此问题。此外,如果您创建实例,则会执行初始化程序。

当然,这并不令人满意,我现在正在添加一些逻辑来决定辅助类型是否可以在检测期间加载以避免此类错误。