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.
2.6 KiB
← transactions · next: production checklist →
11. Seeing what is actually happening
Most caching bugs stop being mysterious the moment you can see the keys. Four things to reach for, in order of how quickly they answer the question.
1. Print the cache
The diagnostic endpoint in this module walks the CacheManager and dumps every entry with the
runtime class of the key and the value. docs/output/23-diagnostics.txt:
"shapes": {
"implementation": "org.springframework.cache.concurrent.ConcurrentMapCache",
"nativeStore": "java.util.concurrent.ConcurrentHashMap",
"entries": {
"SimpleKey [] [SimpleKey]": "zero [String]",
"SimpleKey [abc, 7] [SimpleKey]": "two:abc:7 [String]",
"abc [String]": "one:abc [String]"
}
}
A SimpleKey [] where you expected an id, two methods writing into one key space, or a
NullValue sitting where a record should be — all visible at a glance.
Delete it before shipping. It exposes cached data over HTTP with no authorisation. If you want something permanent, put it behind Actuator's security and return key counts rather than values.
2. Is the bean even proxied?
AopUtils.isAopProxy(bean) // false -> nothing downstream matters
bean.getClass().getName() // ...$$SpringCGLIB$$0
AopUtils.getTargetClass(bean)
If it is proxied and the cache is still empty, the call is not going through the proxy. See chapter 3.
3. Actuator
management.endpoints.web.exposure.include=caches gives /actuator/caches, which lists the
cache names and their CacheManager — enough to confirm which provider is live and whether a
cache name is a typo. DELETE /actuator/caches/{name} clears one, which is a genuinely useful
operational lever.
/actuator/metrics/cache.gets and friends are populated automatically for providers Micrometer
can instrument. Caffeine only reports statistics if the cache was built with recordStats()
— without it the metrics exist and read zero, which looks exactly like a cache nobody is using.
4. Turn the cache off
spring.cache.type=none
A NoOpCacheManager: every method runs every time, annotations untouched. If the bug survives,
it was never the cache. This is the fastest bisect available and it takes one property.
Logging
logging.level.org.springframework.cache=TRACE logs each operation the interceptor resolves.
It is noisy enough that it is a debugging tool rather than something to leave on, but it answers
"did the interceptor see this call at all" definitively.