Bean lifecycle, proxies and AOP

Spring Boot · Expert · 18 min

Most baffling Spring bugs (annotations ignored, beans in odd states) come from lifecycle and proxy details.

Creating a bean goes through defined phases: the container reads bean definitions (from scanning, @Bean methods, auto-configuration), lets BeanFactoryPostProcessors modify the definitions (this is how ${...} placeholders are resolved), then for each singleton: instantiate, inject dependencies, run Aware callbacks, call BeanPostProcessor.postProcessBeforeInitialization, run init callbacks (@PostConstruct, afterPropertiesSet), then postProcessAfterInitialization. On shutdown, @PreDestroy runs.

That last post-processing step is where proxies are created. Features like @Transactional, @Async, @Cacheable, @Validated, @PreAuthorize and custom @Aspects are AOP: a post-processor wraps the bean in a proxy that runs the extra behaviour around method calls. Spring Boot uses CGLIB subclass proxies by default; JDK dynamic proxies are used only for interface-based proxying when configured.

Consequences:

  • Self-invocation: this.other() inside the bean calls the target directly, not the proxy, so other()'s annotations do nothing. Restructure into a separate bean.
  • CGLIB proxies can't intercept final or private methods, and a final class can't be proxied at all.
  • The object injected elsewhere is the proxy, not your instance; getClass() shows a $$SpringCGLIB$$ subclass.

Scopes other than singleton (prototype, request, session) also use proxies when injected into singletons. @Configuration classes are themselves CGLIB-enhanced so that calling one @Bean method from another returns the singleton; proxyBeanMethods = false turns this off for faster startup.

Example

@Aspect
@Component
class TimingAspect {
    @Around("@annotation(com.example.Timed)")
    Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try { return pjp.proceed(); }
        finally {
            System.out.printf("%s took %d us%n", pjp.getSignature().toShortString(),
                (System.nanoTime() - start) / 1000);
        }
    }
}

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Timed {}

@Service
class ReportService {
    @Timed public String build() { return "report " + this.helper(); } // timed
    @Timed public String helper() { return "data"; }  // NOT timed when called via this
    @PostConstruct void init() { System.out.println("ready: " + getClass().getSimpleName()); }
}
// ready: ReportService           (init runs on the target, before proxying)
// injected bean's class: ReportService$$SpringCGLIB$$0
// reportService.build() prints only "ReportService.build() took ... us"

Remember

  • BeanPostProcessors run around init callbacks; proxies are created after initialisation
  • Transactions, async, caching and security are proxy-based AOP
  • Self-invocation, final and private methods bypass proxies
  • @Configuration classes are proxied so @Bean methods return singletons

Watch out for

  • Marking a @Transactional or @Cacheable method private or final, so the proxy can't intercept it.

Check yourself

  1. Why is helper()'s @Timed ignored when build() calls this.helper()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.spring.io/spring-framework/reference/core/aop/proxying.html