Spring Cache Custom KeyGenerator

In this post, we will explore how to create a custom key generator with Spring Cache. Read our article cache Spring Caching the Spring cache introduction.

 

Introduction

Since caches are essentially key-value stores, each invocation of a cached method needs translation into a suitable key for cache access. In this post, we are covering the default key generation features provided by Spring Cache API. We are also going to cover the option to create a custom key generator with Spring Cache.

 

1. KeyGenerator

Spring Cache API uses a simple KeyGenerator for generating a key to store caching data. The default key generators for Spring Cache SimpleKeyGenerator.This default implementation uses the method parameters to generate the key. Here is the high-level overview for the default key generation algorithm.

  • If no params are given, return SimpleKey.EMPTY.
  • With only one parameter, return that instance.
  • If more the one param is given, return a SimpleKey containing all parameters.

Above approach works for most of the use cases, however, there are certain use cases where the above algorithm can cause collision while creating the key.

  •  In the case of two methods with the same parameters and cache name.

Let’s look at an example to understand this more clearly.

@CacheConfig(cacheNames = "customer")
public class CustomerService {

 @Cacheable
 public Customer getCustomer(Integer customerId) {
  return // ...
 }

 @Cacheable
 public EliteCustomer getEliteCustomer(Integer id) {
  return // ...
 }

}

[pullquote align=”normal”]The default key generation strategy changed with the release of Spring 4.0. Earlier versions of Spring used a key generation strategy that, for multiple key parameters, only considered the hashCode() of parameters and not equals();

This could cause unexpected key collisions. The new ‘SimpleKeyGenerator’ uses a compound key for such scenarios. [/pullquote]

 

2. Custom KeyGenerator

Spring Caching API provides options to create a custom key generator for handling all such use cases. To give a different default key generator, we need to implement the org.springframework.cache.interceptor.KeyGenerator interface.KeyGenerator needs to implement a single method.

/**
 * Generate a key for the given method and its parameters.
 * @param target the target instance
 * @param method the method being called
 * @param params the method parameters (with any var-args expanded)
 * @return a generated key
 */
Object generate(Object target, Method method, Object...params);

Let’s take a look at the custom key generator for Spring Caching

public class CustomKeyGenerator implements KeyGenerator {

 @Override
 public Object generate(Object target, Method method, Object...params) {
  return target.getClass().getSimpleName() + "_" + method.getName() + "_" +
   StringUtils.arrayToDelimitedString(params, "_");
 }
}

For more complex use cases, we can create a CustomKey class and implement both equals() and hashCode().

public class CustomCacheKey implements Serializable {

 public static final CustomCacheKey EMPTY = new CustomCacheKey();

 private final Object[] params;
 private final int hashCode;

 public CustomCacheKey(Object...elements) {
  Assert.notNull(elements, "null value");
  this.params = new Object[elements.length];
  System.arraycopy(elements, 0, this.params, 0, elements.length);
  this.hashCode = Arrays.deepHashCode(this.params);
 }

 @Override
 public boolean equals(Object obj) {
  return (this == obj || (obj instanceof CustomCacheKey &&
   Arrays.deepEquals(this.params, ((CustomCacheKey) obj).params)));
 }

 @Override
 public final int hashCode() {
  return this.hashCode;
 }

 @Override
 public String toString() {
  return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";
 }
}

This is how our custom key generator looks like

public class CustomKeyGenerator implements KeyGenerator {

 @Override
 public Object generate(Object target, Method method, Object...params) {
  return generateKey(params);
 }

 /**
  * Generate a key based on the specified parameters.
  */
 public static Object generateKey(Object...params) {
  if (params.length == 0) {
   return CustomCacheKey.EMPTY;
  }
  if (params.length == 1) {
   Object param = params[0];
   if (param != null && !param.getClass().isArray()) {
    return param;
   }
  }
  return new CustomCacheKey(params);
 }
}

 

3. Using Custom KeyGenerator

We have our custom key generator, to use it, we have the following two options to use the custom key in our Spring Cache application.

 

3.1 Use CachingConfigurerSupport

The first option is to implement CachingConfigurer. Recommendation is to extend from CachingConfigurerSupport.

@EnableCaching
@Configuration
public class ApplicationConfig extends CachingConfigurerSupport {

 @Bean("customKeyGenerator")
 public KeyGenerator keyGenerator() {
  return new CustomKeyGenerator();
 }
}

Extending from CachingConfigurerSupport helps in registering the declared KeyGenerator with the interceptors.

 

3.2 Method Level KeyGenerator

If you like to use, a custom key generator for certain methods, pass it as keyGenerator attribute in the @Cacheable annotation.

@Component
public class DefaultProductService implements ProductService {

 @Override
 @Cacheable("products", keyGenerator = "customKeyGenerator")
 public List < Product > getProducts() {

  List < Product > productList = new ArrayList < > ();
  for (int i = 0; i < 9; i++) {
   simulateSlowness();
   productList.add(new Product(String.valueOf(i), "Demo Product", "Sample Description"));
  }
  return productList;
 }
}

 

3.3 SPEL Expressions

The @Cacheable annotation allows the user to specify key generation algorithm through its key attribute. We can use  SpEL to pick the arguments for key generation. Here are some of the examples.

@Cacheable(cacheNames="address", key="#customer")
public Address getAddress(final Customer customer)() {...}

@Cacheable(cacheNames="address", key="#customer.id")
public Address getAddress(final Customer customer)() {...}

[pullquote align=”normal”]The key and keyGenerator parameters are mutually exclusive and an operation specifying both will result in an exception. [/pullquote]

 

Summary

In this article, we explore how to create a custom key generator with Spring Cache. We discuss the features and capabilities of the default key generator. We also discuss the steps of implementing a custom Spring Cache’s KeyGenerator.

You can download the source code for this post from GitHub