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.
4.7 KiB
← sync and async · next: versus Hibernate L2 →
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.