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, soother()'s annotations do nothing. Restructure into a separate bean. - CGLIB proxies can't intercept
finalorprivatemethods, and afinalclass 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
@Configurationclasses 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.