Search code examples
aopspring-aopcglibdynamic-proxy

cglib proxy and null instance variable internals


My query is why is an instance variable null when cglib proxy with spring tries to access it inside a final method. This would mean that even if I had declared the variable directly like in the class below -

class A {
  String prop="a";

  public final eat(){
    return prop;
  }
}

The call to the proxy class's eat() method would return null as the method is final. This seems to imply that when the proxy is initialised , the prop variable is set to null .

My current understanding which I want to validate -

Spring internally seems to use SpringObjenesis#newInstance to create a proxy object [ I debugged through the code ]. This seems to create the class instance with all instance variable set to null. When the proxy now tries to return the instance variable , it will return null.


Solution

  • The way Spring uses CGLIB proxies is by a delegation pattern, see this answer for a schematic code sample about how this is implemented.

    Please understand that when creating the proxy, only method delegation is being considered. The proxy object's instance fields will not be initialised, because usually at some point the proxy calls the delegate's method. Therefore, the delegate will transparently access its own (initialised) fields.

    In case of a final method, however, no proxy method can be generated, because final methods cannot be overridden. I.e., in this case the original method is called, but when accessing fields via e.g. return myField (a shorthand for return this.myField), this is the proxy instance, because there was no method call delegation to the original object. This explains why the result is null, 0 or false, depending on the field type.

    package de.scrum_master.spring.q72993106;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyComponent {
      String food = "bread";
      String beverage = "water";
    
      public final String eat() {
        return food;
      }
    
      public String drink() {
        return beverage;
      }
    }
    

    Add an aspect as a simple way to trigger proxy creation:

    package de.scrum_master.spring.q72993106;
    
    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(* MyComponent.*())")
      public void myAdvice(JoinPoint jp) {
        System.out.println(jp);
      }
    }
    
    package de.scrum_master.spring.q72993106;
    
    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(proxyTargetClass = true)
    public class DemoApplication {
      public static void main(String[] args) throws Throwable {
        try (ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args)) {
          doStuff(appContext);
        }
      }
    
      private static void doStuff(ConfigurableApplicationContext appContext) {
        MyComponent myComponent = appContext.getBean(MyComponent.class);
        System.out.println("Eating " + myComponent.eat());
        System.out.println("Drinking " + myComponent.drink());
      }
    }
    

    Running this application yields the following console log:

    Eating null
    execution(String de.scrum_master.spring.q72993106.MyComponent.drink())
    Drinking water
    

    See? You get the expected value for the non-final method, because delegation to the original object takes place (and the aspect kicks in), but for the final method the result is null, as explained above.