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.
93 lines
4.7 KiB
Markdown
93 lines
4.7 KiB
Markdown
[← sync and async](07-sync-and-async.md) · [next: versus Hibernate L2 →](09-versus-hibernate-l2.md)
|
|
|
|
# 8. Providers, TTL, and the dependency that changes your cache
|
|
|
|
## The abstraction has no TTL
|
|
|
|
None. No time-to-live, no time-to-idle, no maximum size, no eviction policy. Those are provider
|
|
features, and the reference documentation says so plainly.
|
|
|
|
On the default `simple` provider — a `ConcurrentHashMap` behind `ConcurrentMapCache` — an entry
|
|
stays until something evicts it by hand or the process ends. That is fine for a lookup table with
|
|
twelve rows and a slow leak for anything else.
|
|
|
|
With Caffeine configured for `expireAfterWrite=400ms, maximumSize=3`,
|
|
`docs/output/15-providers-and-ttl.txt`:
|
|
|
|
```
|
|
two calls, same key, immediately -> 1 repository calls
|
|
one more call 600 ms later -> 2 repository calls <- the entry expired
|
|
|
|
five distinct keys written, maximumSize = 3
|
|
estimated size after eviction settles : 3
|
|
stats : hits=1 misses=5 evictions=3
|
|
```
|
|
|
|
## Auto-detection, and why your provider changed
|
|
|
|
If there is no `CacheManager` bean and no `cacheResolver`, Spring Boot imports one configuration
|
|
per provider and the first one whose `@ConditionalOnClass` matches registers the manager; the
|
|
rest back off on `@ConditionalOnMissingBean`.
|
|
|
|
The order is worth taking from the enum rather than from prose. `CacheConfigurations` holds an
|
|
`EnumMap<CacheType, String>`, so the iteration order is the declaration order of
|
|
`org.springframework.boot.autoconfigure.cache.CacheType` — which on 4.1.1 is:
|
|
|
|
```
|
|
1 GENERIC 2 JCACHE 3 HAZELCAST 4 COUCHBASE 5 INFINISPAN
|
|
6 REDIS 7 CACHE2K 8 CAFFEINE 9 SIMPLE 10 NONE
|
|
```
|
|
|
|
Spring Boot's reference page lists this as Generic, JCache, Hazelcast, **Infinispan**,
|
|
**Couchbase**, Redis, **Caffeine**, **Cache2k**, Simple. Two pairs are swapped relative to what
|
|
ships, and the second swap is the one that can bite: with both libraries on the classpath you get
|
|
**Cache2k, not Caffeine**. `ProviderDetectionTest` reads `CacheType.values()` rather than
|
|
transcribing a list, so it will notice if this changes again.
|
|
|
|
One more curiosity for anyone chasing Boot 4 package moves: the configurations live in
|
|
`org.springframework.boot.cache.autoconfigure`, but `CacheType` itself stayed behind in
|
|
`org.springframework.boot.autoconfigure.cache`, in `spring-boot-autoconfigure`.
|
|
|
|
That has a consequence worth internalising. This module added Caffeine because one chapter needed
|
|
TTL. Every cache in the application moved off the simple provider as a result, and nothing said
|
|
so — `docs/output/18-provider-detection.txt` catches it:
|
|
|
|
```
|
|
spring.cache.type : (not set)
|
|
resolved CacheManager bean : org.springframework.cache.caffeine.CaffeineCacheManager
|
|
```
|
|
|
|
A transitive dependency on Hazelcast, added for something unrelated, will do the same thing and
|
|
outrank Redis while it is at it. **Set `spring.cache.type` explicitly in anything you deploy.**
|
|
|
|
## Per-provider configuration
|
|
|
|
| Property | Effect |
|
|
|---|---|
|
|
| `spring.cache.type` | `generic`, `jcache`, `hazelcast`, `infinispan`, `couchbase`, `redis`, `caffeine`, `cache2k`, `simple`, `none` |
|
|
| `spring.cache.cache-names` | Creates exactly these caches at startup; anything else fails at the call |
|
|
| `spring.cache.caffeine.spec` | e.g. `maximumSize=500,expireAfterAccess=600s` |
|
|
| `spring.cache.redis.time-to-live` | a `Duration` |
|
|
| `spring.cache.redis.cache-null-values` | default `true` |
|
|
| `spring.cache.redis.key-prefix`, `use-key-prefix` | keyspace hygiene in a shared Redis |
|
|
| `spring.cache.jcache.provider`, `spring.cache.jcache.config` | JSR-107 |
|
|
|
|
`spring.cache.type=none` gives a `NoOpCacheManager`: every method runs every time, and the
|
|
annotations stay where they are. Useful in tests, and the fastest way to answer "is the cache
|
|
causing this?".
|
|
|
|
`spring.cache.cache-names` is worth using in production: it turns a typo in a cache name from a
|
|
cache that silently never hits into an `IllegalArgumentException` at the first call.
|
|
|
|
One caveat — a single `spring.cache.caffeine.spec` applies to **every** cache. Per-cache expiry
|
|
needs your own `CaffeineCacheManager` (or several `CacheManager` beans and `cacheManager = "..."`
|
|
on the operations), which is another argument for one cache name per method.
|
|
|
|
## Where the auto-configuration lives in Boot 4
|
|
|
|
`org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration`, shipped in the
|
|
`spring-boot-cache` module. Boot 4 split `spring-boot-autoconfigure` into per-technology modules;
|
|
the Boot 3 coordinate `org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration` no
|
|
longer resolves. `docs/output/16-autoconfiguration.txt` checks both names against the running
|
|
classpath. It matters if you write `@ImportAutoConfiguration` or exclusions by class name.
|