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: 19 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. - 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.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
[← transactions](10-transactions.md) · [next: production checklist →](12-production-checklist.md)
|
||||
|
||||
# 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`:
|
||||
|
||||
```json
|
||||
"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?
|
||||
|
||||
```java
|
||||
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](03-self-invocation.md).
|
||||
|
||||
## 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
|
||||
|
||||
```properties
|
||||
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.
|
||||
Reference in New Issue
Block a user