[← the three annotations](02-the-three-annotations.md) · [next: keys →](04-keys.md) # 3. The self-invocation trap The symptom: `@Cacheable` is on the method, the application starts cleanly, nothing is logged, and the cache is empty. Or worse, the cache works when the method is called from a controller and does not when it is called from a sibling method three lines away. ## The mechanism `@EnableCaching` registers an auto-proxy creator. The bean the container hands out is a proxy — in this module a CGLIB subclass, printed in `docs/output/03-self-invocation.txt`: ``` injected bean class : com.ankurm.caching.selfinvocation.CatalogService$$SpringCGLIB$$0 is an AOP proxy? : true target class : com.ankurm.caching.selfinvocation.CatalogService ``` The interceptor lives in the proxy. `this.lookup(...)` inside the target object is a plain virtual call on `this`, which is the target, not the proxy. The interceptor is never reached. Measured over four ISBNs with two repeats, so a working cache does two lookups: ``` this.lookup(..) -> 4 repository calls <- no caching at all self.getObject().lookup(..) -> 2 repository calls AopContext.currentProxy() -> 2 repository calls a second bean calls lookup(..) -> 2 repository calls ``` ## The three fixes, ranked **1. Move the call to another bean.** The loop and the cached lookup belong to different responsibilities anyway. No Spring-specific machinery, no cycle, testable in isolation. This is the one to reach for. **2. Inject yourself as an `ObjectProvider`.** ```java private final ObjectProvider self; ... CatalogService proxy = self.getObject(); ``` `ObjectProvider` defers the lookup, so there is no constructor cycle. `@Lazy CatalogService self` works the same way. It is honest about what it is doing, which is more than can be said for the next option. **3. `AopContext.currentProxy()`.** Works, but only when the proxy was created with `exposeProxy` on — and, as [chapter 2](02-the-three-annotations.md) notes, `@EnableCaching` has no such attribute. `@EnableAspectJAutoProxy(exposeProxy = true)` is the usual advice and drags in AspectJ. This module flips the flag on the creator `@EnableCaching` already registered: ```java @Bean static BeanFactoryPostProcessor exposeCachingProxy() { return beanFactory -> { if (beanFactory instanceof BeanDefinitionRegistry registry) { AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); } }; } ``` The cost is a ThreadLocal read and a cast in business code, and it only works inside a call that was itself intercepted. ## The fourth option: stop using proxies `@EnableCaching(mode = AdviceMode.ASPECTJ)` with compile-time or load-time weaving modifies the bytecode, so self-invocation and non-public methods are both intercepted. It is a real answer and almost nobody takes it, because the build complexity is not worth it for caching alone. ## Two relatives of the same bug `docs/output/04-non-public-and-postconstruct.txt`: - **A non-public annotated method is never advised.** In proxy mode the annotation on a `protected`, package-private or `private` method is silently ignored. Two calls, two repository hits, no warning. - **`@PostConstruct` runs before the proxy is in place.** A warm-up loop in an init method warms nothing. The reference documentation says so, and it still catches people. ## How to tell in ten seconds Inject the bean, print `AopUtils.isAopProxy(bean)` and `bean.getClass().getName()`. If the class name has no `$$SpringCGLIB$$` or `$Proxy` in it, there is no interceptor and nothing downstream matters. If it does, but the cache is still empty, the call is not going through it — look for `this.`.