Spring Cache - Part 2 - Cache Keys

In part 1 of the post, we looked at how data can be cached through simple configuration. In this part, we are going to explore the cache keys.

KeyGenerator


Each data item in the cache is stored against a key, which would be used to lookup the data item on retrieval. KeyGenerator is responsible for generating keys for a given data retrieval method.

A KeyGenerator needs to implement just one single method. If it is not implemented or used correctly, it can lead to collisions, which means that the cache data can be overwritten by another method.

//method - is the method on which the cache is enabled
//params - are the parameters of the method.
//target - is the instance of the class to which the method belongs.
Object generate(Object target, Method method, Object... params);


SimpleKeyGenerator is the default generator provided by Spring. Spring automatically configures this as the KeyGenerator if one has not been configured. It uses the method parameters provided to generate the key. If you have two methods that use the same cache name and the same set of parameter types, then there is a good chance that it will result in a collision. In such a scenario, either use different cache names for each method or provide different parameters.

Custom KeyGenerator


You can create and register a custom KeyGenerator, instead of using the default one. For that, first, you need to define a class that implements the KeyGenerator interface, e.g.

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.util.StringUtils;

public class CodelooruKeyGenerator implements KeyGenerator {

 //Generates the same key as SimpleKeyGenerator,
 //except that it prefixes it with classname and method name 
 public Object generate(Object target, Method method, Object... params) {
  return target.getClass().getSimpleName() + "_" + method.getName() + "_"
    + StringUtils.arrayToDelimitedString(params, "_");
 }

}

Now, declare the bean for this generator in the ApplicationConfig. But more importantly, extend the ApplicationConfig from CachingConfigurerSupport (or implement CacheConfigurer). This helps in registering the declared KeyGenerator with the interceptors.

@EnableCaching
@Configuration
public class ApplicationConfig extends CachingConfigurerSupport {
 @Bean
 public CacheManager cacheManager() {
  SimpleCacheManager cacheManager = new SimpleCacheManager();
  Cache actorsCache = new ConcurrentMapCache("actors");
  cacheManager.setCaches(Arrays.asList(actorsCache));
  return cacheManager;
 }
 
 @Bean("codelooruKeyGenerator")
 public KeyGenerator keyGenerator() {
  return new CodelooruKeyGenerator();
 }

}

If you wish to use a custom KeyGenerator only for a certain method, then set the keyGenerator attribute in @Cacheable. e.g. The keyGenerator attribute takes the bean name as a valid value.

@Component
public class ActorService {
 
    @Cacheable(value="actors", keyGenerator="codelooruKeyGenerator")
    public List getActors() {
        System.out.println("----- Getting movie actors ----");
        List<actor> actors = new ArrayList<actor>();
        actors.add(new Actor("Sean Connery", Gender.MALE));
        actors.add(new Actor("Meryl Streep", Gender.FEMALE));
        return actors;
    }
}

SPEL expressions for keys


@Cacheable also accepts a key attribute, which takes a SPEL expression and evaluates against the parameters, and uses that value as the key to store or retrieve from the cache. This overrides the KeyGenerator.

Here are a few examples.
    //This would use the name field from the actor param as the key
    @Cacheable(value = "actors", key = "#actor.name")
    public Actor getActor(Actor actor) {
        return actors.get(actorName);
    }

    //This would use the method name as the key
    @Cacheable(value = "actors", key = "#root.method")
    public List<actor> getActors() {
        return actorDAO.getActors();
    }