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.
46 lines
2.5 KiB
Markdown
46 lines
2.5 KiB
Markdown
[← README](../README.md) · [next: the three annotations →](02-the-three-annotations.md)
|
|
|
|
# 1. What the Spring cache abstraction actually is
|
|
|
|
It is an interceptor and a map interface. That is the whole idea, and holding onto it explains
|
|
almost every surprise later.
|
|
|
|
When a bean carries `@Cacheable`, Spring does not modify the class. It places an AOP proxy in
|
|
front of it and puts a `CacheInterceptor` in the chain. On each call the interceptor:
|
|
|
|
1. asks a `KeyGenerator` for a key,
|
|
2. asks a `Cache` (looked up from a `CacheManager` by name) whether it holds that key,
|
|
3. returns the stored value if it does, and otherwise calls the real method and stores the result.
|
|
|
|
`org.springframework.cache.Cache` is a small interface — `get`, `put`, `evict`, `evictIfPresent`,
|
|
`clear`, `invalidate`, `retrieve`. Everything you associate with a cache product — expiry, size
|
|
limits, eviction policy, replication, persistence, statistics — lives behind that interface in a
|
|
provider. The abstraction itself has none of it. The reference documentation is explicit about
|
|
this in its "How can I set the TTL/TTI/eviction policy" section: you configure it on the provider.
|
|
|
|
## What that buys you
|
|
|
|
Portability of the *declaration*, not the behaviour. The same annotated method runs against a
|
|
`ConcurrentHashMap` in a unit test, Caffeine in one deployment and Redis in another, without the
|
|
service code changing. That is genuinely useful and it is the main reason to use it.
|
|
|
|
## When not to cache
|
|
|
|
- **The method is not slow.** A cache turns a 2 ms call into a 0.1 ms call and adds a correctness
|
|
problem. Measure first.
|
|
- **The data must be correct right now.** Balances, stock levels, permissions. A cache is a
|
|
deliberate decision to serve stale data; make it deliberately.
|
|
- **The hit rate will be low.** A cache keyed on something nearly unique — a search phrase, a
|
|
request id — is a memory leak wearing a performance costume.
|
|
- **The value is huge and the memory budget is not.** On the default provider nothing evicts.
|
|
|
|
`docs/output/01-basics.txt` has the smallest possible demonstration: 200 ms, then 0 ms, with the
|
|
repository's invocation counter proving the method body did not run the second time.
|
|
|
|
## The two caches people confuse
|
|
|
|
If you are using JPA, you already have caching whether you asked for it or not: the persistence
|
|
context (first level) and possibly Hibernate's second-level cache. They are a different thing
|
|
from this, at a different layer, with different failure modes.
|
|
[Chapter 9](09-versus-hibernate-l2.md) is the comparison.
|