问题
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