Search code examples
springspring-bootaop

Why are field values null when calling final methods on a proxy instance?


I have my own Spring Boot application and want to create my annotation, which will check incoming data to null.

When I created the @Aspect annotated class, all other beans became null, what should I do?

@Aspect class code:

@Aspect
@Component
public class EnableRestCallLogAspect {
    @Around("@annotation(EnableRestCallLogs)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        long initTime = System.currentTimeMillis();
        Object proceed = joinPoint.proceed();
        long executionTime = System.currentTimeMillis() - initTime;
        System.out.println("============================================================================================================");
        System.out.println("Method Signature is : "+joinPoint.getSignature() );
        System.out.println("Method executed in : " + executionTime + "ms");
        System.out.println("Input Request: " + joinPoint.getArgs()[0]);
        System.out.println("Output Response : " + proceed);
        return proceed;
    }
}

Annotation EnableRestCallLogs is annotated only in methods and in PostMapping methods:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableRestCallLogs {

}

Solution

  • Preface

    Actually, the question is quite interesting, so I want to explain the situation with a demo. Before talking about this problem, please first read and try to understand my answer to "Why does self-invocation not work for Spring proxies (e.g. with AOP)?". The main take-away it the fact that Spring proxies use a delegation pattern.

    Next, let us talk about auto-wiring: Spring injects the values into the original instance, i.e. the proxy's corresponding field values remain null, because the proxy's sole purpose is to intercept methods, possibly

    • modifying their input parameters or results,
    • decorating their behaviour or completely skipping calling them altogether.

    Spring example

    An entity and its auto-wired dependency:

    package de.scrum_master.spring.q77134178;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyEntity {
      @Autowired
      private MyDependency myDependency;
    
      public void canOverridePublicNonFinal() {
        System.out.println("canOverridePublicNonFinal");
        System.out.println("  class      = " + this.getClass().getName());
        System.out.println("  dependency = " + myDependency);
        cannotOverridePrivate();
      }
    
      private void cannotOverridePrivate() {
        System.out.println("cannotOverridePrivate");
        System.out.println("  class      = " + this.getClass().getName());
        System.out.println("  dependency = " + myDependency);
      }
    
      public final void cannotOverrideFinal() {
        System.out.println("cannotOverrideFinal");
        System.out.println("  class      = " + this.getClass().getName());
        System.out.println("  dependency = " + myDependency);
      }
    }
    
    package de.scrum_master.spring.q77134178;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyDependency {}
    

    An aspect intercepting entity methods:

    Why do we need an aspect? Because

    • it is an easy way to both document which methods can be proxied and intercepted and
    • it triggers proxy creation in the first place.
    package de.scrum_master.spring.q77134178;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class MyAspect {
      @Before("execution(* MyEntity.*(..))")
      public void myAdvice(JoinPoint joinPoint) {
        System.out.println(joinPoint);
      }
    }
    

    A driver application:

    package de.scrum_master.spring.q77134178;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @SpringBootApplication
    @Configuration
    @EnableAspectJAutoProxy
    public class DemoApplication {
      public static void main(String[] args) {
        try (ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args)) {
          MyEntity myEntity = context.getBean(MyEntity.class);
          myEntity.canOverridePublicNonFinal();
          myEntity.cannotOverrideFinal();
        }
      }
    }
    

    Console log:

    execution(void de.scrum_master.spring.q77134178.MyEntity.canOverridePublicNonFinal())
    canOverridePublicNonFinal
      class      = de.scrum_master.spring.q77134178.MyEntity
      dependency = de.scrum_master.spring.q77134178.MyDependency@6c24f61d
    cannotOverridePrivate
      class      = de.scrum_master.spring.q77134178.MyEntity
      dependency = de.scrum_master.spring.q77134178.MyDependency@6c24f61d
    cannotOverrideFinal
      class      = de.scrum_master.spring.q77134178.MyEntity$$EnhancerBySpringCGLIB$$e1109d0e
      dependency = null
    

    Please note:

    • The aspect is only triggered once for method canOverridePublicNonFinal(), because it is the only entity method which is neither final nor private. This is easy to understand, because in Java private or final methods cannot be overridden. Therefore, they cannot be proxied either.

    • The public, non-final method can "see" the injected dependency, because the proxy delegates to the original method, hence this.myDependency or simply myDependency refers to the auto-wired dependency.

    • The private method also can "see" the injected dependency, because the method only exists in the original class and no proxying is going on there, hence this.myDependency or simply myDependency refers to the auto-wired dependency.

    • The final method, however, both is callable from outside, i.e. callable on the proxy instance, and cannot be proxied, which means that this.myDependency or simply myDependency refers to the proxy instance(!) itself, which finally yields null when trying to access the field.

    POJO example emulating Spring's dynamic proxies

    Here is a plain Java version (no Spring or dynamic proxies), emulating the behaviour in POJO fashion in order to hopefully make it even clearer what goes on behind the curtains in Spring AOP.

    Entity and injectable dependency:

    You see, the classes are identical to the Spring example. I just removed @Component and @Autowired and added a setter method to enable dependency injection.

    package de.scrum_master.app;
    
    public class MyEntity {
      private MyDependency myDependency;
    
      public void setMyDependency(MyDependency myDependency) {
        this.myDependency = myDependency;
      }
    
      public void canOverridePublicNonFinal() {
        System.out.println("canOverridePublicNonFinal");
        System.out.println("  class      = " + this.getClass().getName());
        System.out.println("  dependency = " + myDependency);
        cannotOverridePrivate();
      }
    
      private void cannotOverridePrivate() {
        System.out.println("cannotOverridePrivate");
        System.out.println("  class      = " + this.getClass().getName());
        System.out.println("  dependency = " + myDependency);
      }
    
      public final void cannotOverrideFinal() {
        System.out.println("cannotOverrideFinal");
        System.out.println("  class      = " + this.getClass().getName());
        System.out.println("  dependency = " + myDependency);
      }
    }
    
    package de.scrum_master.app;
    
    public class MyDependency {}
    

    Pseudo proxy class:

    Here we see how delegation works in Spring AOP.

    package de.scrum_master.app;
    
    public class MyEntityProxy extends MyEntity {
      private MyEntity delegate;
    
      public MyEntityProxy(MyEntity delegate) {
        this.delegate = delegate;
      }
    
      @Override
      public void canOverridePublicNonFinal() {
        System.out.println("MyEntityProxy.canOverridePublicNonFinal delegating to original method (emulating aspect or interceptor)");
        delegate.canOverridePublicNonFinal();
      }
    }
    

    Driver application:

    Here, we emulate dependency injection by simply wiring the dependency into the entity manually via setter call.

    package de.scrum_master.app;
    
    public class DemoApplication {
      public static void main(String[] args) {
        // Create entity
        MyEntity myEntity = new MyEntity();
        // Pseudo "auto-wire" a value into the entity
        myEntity.setMyDependency(new MyDependency());
        // Create pseudo "dynamic proxy" for entity
        MyEntityProxy myEntityProxy = new MyEntityProxy(myEntity);
    
        // Call proxy method overriding and delegating to original entity parent method
        myEntityProxy.canOverridePublicNonFinal();
        // Call parent method (final -> cannot be overridden and therefore not intercepted by the proxy)
        myEntityProxy.cannotOverrideFinal();
      }
    }
    

    Console log:

    Unsurprisingly, the console log looks just like for the Spring AOP example, only the proxy class has a less cryptic name, because we manually created a pseudo proxy instead of relying on a CGLIB proxy.

    MyEntityProxy.canOverridePublicNonFinal delegating to original method (emulating aspect or interceptor)
    canOverridePublicNonFinal
      class      = de.scrum_master.app.MyEntity
      dependency = de.scrum_master.app.MyDependency@4554617c
    cannotOverridePrivate
      class      = de.scrum_master.app.MyEntity
      dependency = de.scrum_master.app.MyDependency@4554617c
    cannotOverrideFinal
      class      = de.scrum_master.app.MyEntityProxy
      dependency = null