springspring-aopspring-transactionscglibdynamic-proxy

Self-invocation behaviour in @Configuration vs. @Component classes


My question is about AOP Spring behaviour in case of internal method calls.

@Service
class Service {
    @Transactional
    public void method1() {
        method2();
    }

    @Transactional
    public void method2() {}
}

If we call method1() from outside, method1() will be executed in a transaction mode, but as it calls internally method2(), the code inside method2() will not be executed in a transaction mode.

In parallel, for a Configuration class, normally we should have the same behaviour:

@Configuration
class MyConfiguration{
    @Bean
    public Object1 bean1() {
        return new Object1();
    }

    @Bean
    public Object1 bean2() { 
        Object1 b1 = bean1();
        return new Object2(b1);
    }
}

Normally if i understood well, the call call to bean1() method from bean2() should not be intercepted by the proxy object and hence, if we call bean1() many times, we should get different object every time.

Firstly, could you explains technically why the inner calls are not intercepted by the proxy object, and secondly to check if my understanding of the second example is correct.


Solution

  • Regular Spring @Components

    For an explanation of how normal Spring (AOP) proxies or dynamic proxies (JDK, CGLIB) in general work, see my other answer with illustrative sample code. Read that first and you will understand why self-invocation cannot be intercepted for these types of proxies via Spring AOP.

    @Configuration classes

    As for @Configuration classes, they work differently. In order to avoid Spring beans which have already been created from being created again just because their @Bean factory methods are being called again ex- or internally, Spring creates special CGLIB proxies for them.

    One of my config classes looks like this:

    package spring.aop;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan
    public class ApplicationConfig {
      @Bean(name = "myInterfaceWDM")
      public MyInterfaceWithDefaultMethod myInterfaceWithDefaultMethod() {
        MyClassImplementingInterfaceWithDefaultMethod myBean = new MyClassImplementingInterfaceWithDefaultMethod();
        System.out.println("Creating bean: " + myBean);
        return myBean;
      }
    
      @Bean(name = "myTestBean")
      public Object myTestBean() {
        System.out.println(this);
        myInterfaceWithDefaultMethod();
        myInterfaceWithDefaultMethod();
        return myInterfaceWithDefaultMethod();
      }
    }
    

    The corresponding application looks like this:

    package spring.aop;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    
    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args);
            MyInterfaceWithDefaultMethod myInterfaceWithDefaultMethod =
              (MyInterfaceWithDefaultMethod) appContext.getBean("myInterfaceWDM");
            System.out.println(appContext.getBean("myTestBean"));
        }
    }
    

    This prints (edited to remove stuff we don't want to see):

      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v1.5.2.RELEASE)
    
    2019-07-07 08:37:55.750  INFO 22656 --- [           main] spring.aop.DemoApplication               : Starting DemoApplication on (...)
    (...)
    Creating bean: spring.aop.MyClassImplementingInterfaceWithDefaultMethod@7173ae5b
    spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a@72456279
    

    When running the application, method myInterfaceWithDefaultMethod() is not called multiple times even though there are multiple calls from within myTestBean(). Why?

    You learn more if you put a breakpoint onto one of the myInterfaceWithDefaultMethod() calls within myTestBean() and let the debugger stop there. Then you can inspect the situation by evaluating code:

    System.out.println(this);
    
    spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a@72456279
    

    So the config class is indeed a CGLIB proxy. But what methods does it have?

    for (Method method: this.getClass().getDeclaredMethods()) {
      System.out.println(method);
    }
    
    public final java.lang.Object spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.myTestBean()
    public final spring.aop.MyInterfaceWithDefaultMethod spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.myInterfaceWithDefaultMethod()
    public final void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.setBeanFactory(org.springframework.beans.factory.BeanFactory) throws org.springframework.beans.BeansException
    final spring.aop.MyInterfaceWithDefaultMethod spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$myInterfaceWithDefaultMethod$1()
    public static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$SET_THREAD_CALLBACKS(org.springframework.cglib.proxy.Callback[])
    public static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$SET_STATIC_CALLBACKS(org.springframework.cglib.proxy.Callback[])
    public static org.springframework.cglib.proxy.MethodProxy spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$findMethodProxy(org.springframework.cglib.core.Signature)
    final void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$setBeanFactory$6(org.springframework.beans.factory.BeanFactory) throws org.springframework.beans.BeansException
    static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$STATICHOOK4()
    private static final void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$BIND_CALLBACKS(java.lang.Object)
    final java.lang.Object spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$myTestBean$0()
    static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$STATICHOOK3()
    

    This looks kinda messy, let's just print the method names:

    for (Method method: this.getClass().getDeclaredMethods()) {
      System.out.println(method.name);
    }
    
    myTestBean
    myInterfaceWithDefaultMethod
    setBeanFactory
    CGLIB$myInterfaceWithDefaultMethod$1
    CGLIB$SET_THREAD_CALLBACKS
    CGLIB$SET_STATIC_CALLBACKS
    CGLIB$findMethodProxy
    CGLIB$setBeanFactory$6
    CGLIB$STATICHOOK4
    CGLIB$BIND_CALLBACKS
    CGLIB$myTestBean$0
    CGLIB$STATICHOOK3
    

    Does that proxy implement any interfaces?

    for (Class<?> implementedInterface : this.getClass().getInterfaces()) {
      System.out.println(implementedInterface);
    }
    
    interface org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration
    

    Okay, interesting. Let us read some Javadoc. Actually class ConfigurationClassEnhancer is package-scoped, so we have to read the Javadoc right inside the source code:

    Enhances Configuration classes by generating a CGLIB subclass which interacts with the Spring container to respect bean scoping semantics for @Bean methods. Each such @Bean method will be overridden in the generated subclass, only delegating to the actual @Bean method implementation if the container actually requests the construction of a new instance. Otherwise, a call to such an @Bean method serves as a reference back to the container, obtaining the corresponding bean by name.

    The inner interface EnhancedConfiguration is actually public, but still the Javadoc is again only in the source code:

    Marker interface to be implemented by all @Configuration CGLIB subclasses. Facilitates idempotent behavior for enhance through checking to see if candidate classes are already assignable to it, e.g. have already been enhanced. Also extends BeanFactoryAware, as all enhanced @Configuration classes require access to the BeanFactory that created them.

    Note that this interface is intended for framework-internal use only, however must remain public in order to allow access to subclasses generated from other packages (i.e. user code).

    Now what do we see if we step into the myInterfaceWithDefaultMethod() call? The generated proxy method calls method ConfigurationClassEnhancer.BeanMethodInterceptor.intercept(..) and that method's Javadoc says:

    Enhance a @Bean method to check the supplied BeanFactory for the existence of this bean object.

    There you can see the rest of the magic happening, but the description would really be out of scope of this already lengthy answer.