Files
spring-boot-demo/caching/docs/11-diagnostics.md
T
asmhatre a9867c0423 Add caching: the Spring cache abstraction, keys, eviction timing and the self-invocation trap
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.
2026-09-12 05:35:13 +00:00

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.