Search code examples
javabytecodebyte-buddy

How to intercept a constructor


I want to intercept all methods who are annotated with @Inject. The following test shows that it works fine with methods but it does not with constructors. What am I missing?

I tried to add a custom method matcher and I noticed I was never given a MethodDescription corresponding to the constructor.

public class InterceptConstructorTest {

    @Test
    public void testConstructorInterception() {

        ByteBuddyAgent.install();

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

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

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

        // Call constructor => NOT intercepted 
        MyClass myClass = new MyClass("a param");

        // Call method => intercepted
        myClass.aMethod("a param");
    }
}

class MyClass {

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

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

class MethodInterceptor {

    public static void intercept(@Origin Method method) {
        System.out.println("Intercepted: " + method.getName());
    }
}

Output:

constructor called
Intercepted: aMethod
aMethod called

Solution

  • You specify explicitly that you only want to intercept methods:

    builder.method(isAnnotatedWith(Inject.class))
    

    You can similarly do:

    builder.constructor(isAnnotatedWith(Inject.class))
    

    Or even:

    builder.invokeable(isAnnotatedWith(Inject.class))
    

    Here is the catch, however. Any constructor must call another constructor from within the constructor that is intercepted. In your case, this is already given by using SuperMethodCall.INSTANCE and your code will run. Be however careful that some constructs are not available for constructors, for example, you can not inject a @This reference before the super constructor is called. If appropriate, you would for example switch:

    MethodDelegation.to(MethodInterceptor.class)
                    .andThen(SuperMethodCall.INSTANCE)
    

    to become

    SuperMethodCall.INSTANCE
                   .andThen(MethodDelegation.to(MethodInterceptor.class))
    

    When using this ordering, the JVM does no longer complain if inject properties of the intercepted instance.

    Finally, make sure that you offer an appropriate interception:

    class MethodInterceptor {
    
        public static void intercept(@Origin Method method) {
            System.out.println("Intercepted: " + method.getName());
        }
    
        public static void intercept(@Origin Constructor<?> constructor) {
            System.out.println("Intercepted: " + constructor.getName());
        }
    }
    

    Otherwise, Byte Buddy cannot bind a Constructor reference to a Method and discards the method from binding (since 0.7.6, before, there is a bug where a verifier error is caused instead.) When using Java 8, you can also offer a single interceptor using the Executable type.