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.
2.3 KiB
← eviction · next: sync and async →
6. condition, unless, and what a cached null is
The two vetoes
condition is evaluated on the arguments before the method runs. A false condition skips the
lookup and the write — the method behaves as though it were not annotated.
unless is evaluated after, can see #result, and vetoes the write only. The lookup still
happened, so a cached value is still returned on a hit.
@Cacheable(cacheNames = "terms", condition = "#term.length() <= 8")
public String search(String term) { ... }
docs/output/09-conditions.txt: six characters, two calls, one invocation. Twenty-five
characters, two calls, two invocations.
The useful pattern is exactly that one — refuse to cache inputs that will never repeat. A search box keyed on free text has a hit rate close to zero and will happily fill the heap.
null
By default a null return is cached. It is stored as a sentinel,
org.springframework.cache.support.NullValue.INSTANCE, so that a hit on null is
distinguishable from a miss. docs/output/10-nulls.txt shows it in the map:
cache "nulls":
key xyz -> null [org.springframework.cache.support.NullValue]
This is usually what you want. Caching "not found" is the cheap defence against a hot lookup for a row that does not exist — the classic cache-penetration attack is a flood of requests for ids that are not in the database, and a cache that refuses to store misses passes every one of them straight through.
Turn it off when a null means "not loaded yet" rather than "not there":
@Cacheable(cacheNames = "terms", unless = "#result == null")
Or at the manager: ConcurrentMapCacheManager.setAllowNullValues(false), reachable through a
CacheManagerCustomizer. Redis has its own switch, spring.cache.redis.cache-null-values,
default true.
Note the asymmetry that catches people: unless = "#result == null" still performs the lookup,
so if a null got into the cache some other way it will still be served. condition cannot help
here — it cannot see the result.
For an Optional-returning method, #result is the unwrapped value, so the safe-navigation form
is what you want: unless = "#result?.hardback".