Files
spring-boot-demo/caching/README.md
T
asmhatre 66208bcd97 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.
2026-09-12 05:19:22 +00:00

104 lines
6.1 KiB
Markdown

# caching
Companion project for **[The Spring Cache Abstraction: @Cacheable, @CacheEvict, Key Generators
and the Self-Invocation Trap](https://ankurm.com/)** on [ankurm.com](https://ankurm.com).
Every number, key dump, stack trace and error message quoted in the article came out of
`docs/output/`, and every one of those files is regenerated by one script. Most of them are
produced by the test suite, so if a claim stops being true the build goes red.
## Versions
| | |
|---|---|
| Spring Boot | 4.1.1 |
| Spring Framework | 7.0.9 |
| Caffeine | managed by Boot |
| JDK | 25 (Temurin 25.0.4.1+1) |
| Maven | 3.9 |
## Quickstart
```bash
export JAVA_HOME=/path/to/jdk-25
mvn -DskipTests package
./scripts/run-all.sh # regenerates every file under docs/output/
```
Run the application on its own to poke at the diagnostics endpoint:
```bash
java -jar target/caching-1.0.0.jar --spring.cache.type=simple
curl -s localhost:8080/diag/warm
curl -s localhost:8080/diag/caches | jq .
```
## Profiles
| Profile | What it changes |
|---|---|
| *(none)* | Boot's auto-detected provider. Caffeine is on the classpath, so that is what you get |
| `caffeine` | An explicit `CaffeineCacheManager` with `expireAfterWrite=400ms`, `maximumSize=3`, `recordStats()` and `setAsyncCacheMode(true)` |
| `txaware` | `TransactionAwareCacheManagerProxy` over `ConcurrentMapCacheManager` |
Most tests pin `spring.cache.type=simple` so the key dumps show a real `ConcurrentHashMap`.
## Endpoints
| Endpoint | Purpose |
|---|---|
| `GET /diag/warm` | Calls a few cached methods so there is something to look at |
| `GET /diag/caches` | Every cache, every key, with the runtime class of key and value |
| `GET /actuator/caches` | Boot's own view: cache names and their manager |
| `GET /actuator/metrics/cache.gets` | Hit/miss counters, where the provider reports them |
`/diag/*` is a debugging aid with no authorisation. Delete it before shipping anything.
## Documentation
| Chapter | Covers |
|---|---|
| [01 — What the cache abstraction is](docs/01-what-caching-is.md) | the interceptor and the `Cache` interface; when not to cache |
| [02 — The annotations, attribute by attribute](docs/02-the-three-annotations.md) | every attribute from `javap`; five declarations that are rejected and when |
| [03 — The self-invocation trap](docs/03-self-invocation.md) | why `this.method()` caches nothing, three fixes ranked, non-public methods, `@PostConstruct` |
| [04 — Keys](docs/04-keys.md) | `SimpleKeyGenerator`'s three rules, the collision it makes easy, the SpEL surface, mutable keys |
| [05 — Eviction](docs/05-eviction.md) | `beforeInvocation`, what a thrown exception does, `allEntries`, `CacheErrorHandler` |
| [06 — Conditions and nulls](docs/06-conditions-and-nulls.md) | `condition` vs `unless`, `NullValue`, cache penetration |
| [07 — `sync` and async](docs/07-sync-and-async.md) | the stampede measured, the four `sync=true` restrictions, `CompletableFuture` |
| [08 — Providers and TTL](docs/08-providers-and-ttl.md) | detection order, the dependency that silently changes your provider, per-provider properties |
| [09 — This is not the Hibernate L2 cache](docs/09-versus-hibernate-l2.md) | three layers compared; why caching an entity gives you a shared detached object |
| [10 — Transactions](docs/10-transactions.md) | a rollback that does not roll the cache back; `TransactionAwareCacheManagerProxy` and a correction to a widely repeated claim |
| [11 — Diagnostics](docs/11-diagnostics.md) | four ways to see what the cache is doing |
| [12 — Before production](docs/12-production-checklist.md) | the checklist, and whether to cache at all |
## Captured output
| File | What it shows |
|---|---|
| [`01-basics.txt`](docs/output/01-basics.txt) | 200 ms, then 0 ms, with the invocation counter |
| [`02-put-evict-clear.txt`](docs/output/02-put-evict-clear.txt) | the three annotations against one cache |
| [`03-self-invocation.txt`](docs/output/03-self-invocation.txt) | 4 vs 2 repository calls, four call styles |
| [`04-non-public-and-postconstruct.txt`](docs/output/04-non-public-and-postconstruct.txt) | a protected method silently uncached; `@EnableCaching`'s real attributes |
| [`05-key-shapes.txt`](docs/output/05-key-shapes.txt) | zero, one and two arguments in one map |
| [`06-key-collision.txt`](docs/output/06-key-collision.txt) | one method serving another method's answer |
| [`07-mutable-key.txt`](docs/output/07-mutable-key.txt) | two entries, one unreachable |
| [`08-evict-timing.txt`](docs/output/08-evict-timing.txt) | a failed update leaving a stale entry |
| [`09-conditions.txt`](docs/output/09-conditions.txt) | `condition` vetoing before the call |
| [`10-nulls.txt`](docs/output/10-nulls.txt) | `NullValue` in the map |
| [`11-stampede.txt`](docs/output/11-stampede.txt) | 16 invocations vs 1 |
| [`12-async-return-types.txt`](docs/output/12-async-return-types.txt) | `CompletableFuture` with async cache mode on |
| [`13-transactions.txt`](docs/output/13-transactions.txt) | the cache keeping a rolled-back value |
| [`14-cached-entity.txt`](docs/output/14-cached-entity.txt) | `LazyInitializationException` from a cached entity |
| [`15-providers-and-ttl.txt`](docs/output/15-providers-and-ttl.txt) | Caffeine expiry, size bound and stats |
| [`16-autoconfiguration.txt`](docs/output/16-autoconfiguration.txt) | what Boot wired, and where the auto-configuration lives in Boot 4 |
| [`17-async-cache-mode-missing.txt`](docs/output/17-async-cache-mode-missing.txt) | the runtime error a clean startup hides |
| [`18-provider-detection.txt`](docs/output/18-provider-detection.txt) | the provider nobody chose |
| [`19-transaction-aware.txt`](docs/output/19-transaction-aware.txt) | the deferred put, and the evict that is not deferred |
| [`20-invalid-declarations.txt`](docs/output/20-invalid-declarations.txt) | five rejected declarations, four of them at the first call |
| [`22-decorator-bytecode.txt`](docs/output/22-decorator-bytecode.txt) | `javap` proving why `beforeInvocation` is not deferred |
| [`23-diagnostics.txt`](docs/output/23-diagnostics.txt) | every cache, every key, live |
## Licence
MIT — see [LICENSE](../LICENSE).