A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 20 tests producing 22 transcripts under docs/output/, plus 12 documentation chapters. Findings the build pins: - @EnableCaching has no exposeProxy attribute; the widely-copied @EnableCaching(exposeProxy = true) does not compile. - Two methods sharing a cache name and an argument type share a key space, and one silently serves the other's answers. - The documented cache-provider detection order does not match CacheType's enum order in 4.1.1: COUCHBASE before INFINISPAN, and CACHE2K before CAFFEINE. - beforeInvocation = true is NOT deferred by TransactionAwareCacheManagerProxy on 7.0.9 - doEvict picks evictIfPresent, which the decorator does not intercept. - Four of five invalid declarations start a clean context and throw at the first call. - Caffeine on the classpath silently displaces the simple provider.
92 lines
3.8 KiB
Markdown
92 lines
3.8 KiB
Markdown
[← 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<CatalogService> 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.`.
|