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

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.

Leave a Comment