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.
104 lines
6.2 KiB
Markdown
104 lines
6.2 KiB
Markdown
# caching
|
|
|
|
Companion project for **[The Spring Cache Abstraction: @Cacheable, @CacheEvict, Key Generators
|
|
and the Self-Invocation Trap](https://ankurm.com/spring-cache-abstraction-cacheable-cacheevict-self-invocation-trap/)** 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, and the detection order read out of the enum (it disagrees with the docs) |
|
|
| [`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).
|