Files
spring-boot-demo/caching/docs/06-conditions-and-nulls.md
T
asmhatre a9867c0423 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: 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.
2026-09-12 05:35:13 +00:00

56 lines
2.3 KiB
Markdown

[← eviction](05-eviction.md) · [next: sync and async →](07-sync-and-async.md)
# 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.
```java
@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":
```java
@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"`.