Search code examples
javaaspectj

How to intercept constructor call and insert another object with AspectJ?


I want to intercept object creation for a certain class using AspectJ, and insert some other object.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AnnotationAspect {

    @Around("execution(ExampleClass.new())")
    public Object aroundObjectCreation(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Intercepting object creation");
        return null;
    }
}

When I create an object in the main program:

public class Test {

    public static void main(String[] args) {
        ExampleClass e = new ExampleClass();
        System.out.println(e);
    }

}

instead of the expected null value, I get the following output:

Intercepting object creation
com.example.ExampleClass@19bb07ed

How is this possible? The program never reached the real ExampleClass constructor, yet somehow the object is created.


Solution

  • That is an interesting question, so I want to answer it a bit more extensively. As a reference for the additional pointcut types I am going to mention, please use this overview from the AspectJ Programming Guide.

    For constructor interception, there are 4 relevant pointcut types:

    • call: marks the place in the code where the constructor is called
    • execution: marks the place in the code where the constructor is executed
    • preinitialization: encompasses the entry of the first-called constructor to the call to the super constructor
    • initialization: encompasses the return from the super constructor call to the return of the first-called constructor

    Please note that while for the former two, an around advice is possible, for the latter two only before/after advice types are permitted, i.e. you cannot stop them from being executed by not proceeding. I.e., you cannot just return null from a constructor call, because the JVM is not designed that way.

    First, let us extend the sample code, so we can see more:

    package de.scrum_master.stackoverflow.q77438354;
    
    public class BaseClass {
      final int id;
    
      public BaseClass(int id) {
        System.out.println("In BaseClass constructor");
        this.id = id;
      }
    }
    
    package de.scrum_master.stackoverflow.q77438354;
    
    public class ExampleClass extends BaseClass {
      final String name;
    
      public ExampleClass() {
        super(11);
        System.out.println("In ExampleClass constructor");
        name = "Jane Doe";
      }
    
      @Override
      public String toString() {
        return "ExampleClass{id=" + id + ", name=" + name + "}";
      }
    }
    
    package de.scrum_master.stackoverflow.q77438354;
    
    public class Application {
      public static void main(String[] args) {
        System.out.println(new ExampleClass());
      }
    }
    
    package de.scrum_master.stackoverflow.q77438354;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    public class ConstructorInterceptorAspect {
      @Around("execution(ExampleClass.new())")
      public Object aroundObjectCreation(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("[B] " + joinPoint + " -> " + joinPoint.getTarget());
        try {
          System.out.println("Intercepting object creation");
          return null;
        } finally {
          System.out.println("[A] " + joinPoint + " -> " + joinPoint.getTarget());
        }
      }
    
      @Before("preinitialization(ExampleClass.new()) || initialization(ExampleClass.new()) || call(ExampleClass.new())")
      public void beforeInit(JoinPoint joinPoint) {
        System.out.println("[B] " + joinPoint + " -> " + joinPoint.getTarget());
      }
    
      @After("preinitialization(ExampleClass.new()) || initialization(ExampleClass.new()) || call(ExampleClass.new())")
      public void afterInit(JoinPoint joinPoint) {
        System.out.println("[A] " + joinPoint + " -> " + joinPoint.getTarget());
      }
    }
    

    The console log looks as follows:

    [B] call(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> null
    [B] preinitialization(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> null
    [A] preinitialization(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> null
    In BaseClass constructor
    [B] initialization(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> ExampleClass{id=11, name=null}
    [B] execution(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> ExampleClass{id=11, name=null}
    Intercepting object creation
    [A] execution(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> ExampleClass{id=11, name=null}
    [A] initialization(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> ExampleClass{id=11, name=null}
    [A] call(de.scrum_master.stackoverflow.q77438354.ExampleClass()) -> null
    ExampleClass{id=11, name=null}
    

    Please note:

    • Before execution is even reached, preinitialization has finished already and initialization wraps execution, i.e. it starts before execution and ends later.
    • That means e.g., that the super constructor has finished running. You can see in the log ExampleClass{id=11, name=null} at that time, i.e. the super class field was initialised already, while the subclass field has no value yet.
    • Further above, you see null, i.e. during pre-initialisation there is not even an instance constructed yet.
    • Next, you see how the non-proceeding execution advice successfully blocks field initialisation in the intercepted constructor, i.e. the field value remains name=null. BTW, the same way you could also prohibit field initialisation in the super class constructor, if you would change your around execution pointcut correspondingly.

    Actually, your log output "Intercepting object creation" is not entirely correct. You are just intercepting constructor execution, but there is no way to stop object creation and return null instead. You could only throw an exception to stop the object from being created.

    The reason that you can return null in the around call advice is that the object has already been created, i.e. execution has finished, but you simply replace the created value by null.