[← self-invocation](03-self-invocation.md) · [next: eviction →](05-eviction.md) # 4. Keys ## What the default generator builds `SimpleKeyGenerator` has three rules: | Arguments | Key | |---|---| | none | `SimpleKey.EMPTY` | | one | that argument, unwrapped | | two or more | a `SimpleKey` holding all of them | `docs/output/05-key-shapes.txt` shows all three in one map: ``` key abc [String] -> one:abc key SimpleKey [abc, 7] [SimpleKey] -> two:abc:7 key SimpleKey [] [SimpleKey] -> zero ``` The method name and the declaring class appear nowhere. That is the single most consequential fact about Spring cache keys. Before Spring 4.0 the strategy hashed the arguments, which produced real collisions (spring-framework#14870). `SimpleKey` holds the arguments and compares them with `equals`, so arguments need working `equals` and `hashCode` — records and value objects are fine, JPA entities with identity-based equality usually are not. ## The collision this makes easy Two methods, one cache name, same argument type. From `docs/output/06-key-collision.txt`: ``` countLetters("a1b2") -> letters=2 (calls: letters=1 digits=0) countDigits("a1b2") -> letters=2 (calls: letters=1 digits=0) ``` `countDigits` never ran. It found key `"a1b2"` populated and returned the answer to a different question. In production this looks like `findByIsbn` and `findByTitle` next to each other, both annotated `@Cacheable("books")`, both taking a `String`. No-argument methods collide harder still: every one of them keys on `SimpleKey.EMPTY`, so two `@Cacheable("noargs")` methods with no parameters are guaranteed to shadow each other. Three ways out, in order of preference: 1. **One cache name per method.** Free, obvious in a dump, and gives you per-method TTL and per-method metrics as a bonus. 2. **Put the method into the key**: `key = "'letters:' + #input"`. 3. **A custom `KeyGenerator`** that includes `method.getName()`. Applies everywhere at once, which is useful in a large codebase and a blunt instrument in a small one. ## SpEL you can use in `key`, `condition` and `unless` | Expression | What it is | |---|---| | `#argName` | a method argument by name (needs `-parameters`, which Spring Boot's Maven parent sets) | | `#a0`, `#p0` | the same argument by index, when names are unavailable | | `#root.methodName`, `#root.method` | the method being invoked | | `#root.target`, `#root.targetClass` | the target object and its class | | `#root.args[0]` | arguments as an array | | `#root.caches[0].name` | the caches this operation resolves to | | `#result` | the return value — `unless`, `@CachePut` keys, and `@CacheEvict` with `beforeInvocation = false` only | For an `Optional` return, `#result` is the unwrapped object, not the `Optional`. ## Keys you can lose `docs/output/07-mutable-key.txt` passes an `ArrayList` and then mutates it: ``` cache "mutable": key [java, spring] [ArrayList] -> tags=[java] key [java, spring] [ArrayList] -> tags=[java, spring] ``` Two entries whose keys print identically, because they are the same object. The first entry sits under a `hashCode` the map no longer agrees with: unreachable, unevictable, and it will still be there at the next heap dump. Keys must be immutable. If an argument is a collection, copy it or derive a string. ## Keys in a distributed cache Everything above assumes an in-process map. Once the cache is Redis or Hazelcast, two more rules apply: - **The key has to survive being turned into a string or bytes.** This repository has no Redis in it, so treat what follows as reasoning from the API rather than something measured here: `RedisCache` converts the key object through a `ConversionService` before writing it, so a key type it cannot convert fails at the call rather than at startup. An explicit `key` expression that produces a short deterministic `String` sidesteps the question entirely, and it is what you want in a shared cache anyway. - **The key must be stable across deployments.** Anything derived from a hash code, an enum ordinal, or a class name you might refactor will silently miss for the whole cache after a rename, and the old entries will sit there until they expire.