Spring Cache is nothing but a store of key-value pairs, where values are the ones returned from @Cacheable methods, whereas, for keys there has to be some strategy to generate them. By default Spring uses a simple key generation based on the following algorithm:
-
If @Cacheable method has no arguments then SimpleKey.EMPTY is used as key.
-
If only one argument is used, then the argument instance is used as key.
-
If more than one argument is used, then an instance of SimpleKey composed of all arguments is used as key.
Note that to avoid key collisions, SimpleKey class overrides hashCode() and equals() methods and compares all parameters of the target method as shown in the following snippets:
public class SimpleKey implements Serializable {
....
public SimpleKey(Object... elements) {
.....
this.hashCode = Arrays.deepHashCode(this.params);
}
@Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof SimpleKey
&& Arrays.deepEquals(this.params, ((SimpleKey) obj).params)));
}
@Override
public final int hashCode() {
return this.hashCode;
}
......
}
Example
In this example, we will understand the default key generation. We will also understand why we may need custom keys in some cases and how to generate them.
Java Config
@Configuration
@ComponentScan
@EnableCaching
public class AppConfig {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager =
new ConcurrentMapCacheManager("employee-cache");//vararg constructor
return cacheManager;
}
}
ConcurrentMapCacheManager is another Spring provided CacheManager implementation that lazily builds ConcurrentMapCache instances for each request.
A service bean
To understand default cache generation (as stated above), we are going to create a no arg, one arg and multiple args @Cacheable methods:
@Service
@CacheConfig(cacheNames = "employee-cache")
public class EmployeeService {
@Cacheable
public String[] getDepartments() {
System.out.println("getDepartments() invoked");
return new String[]{"IT", "Admin", "Account"};
}
@Cacheable
public Employee getEmployeeById(int id) {
System.out.println("getEmployeeById() invoked");
return new Employee(id, "Adam", "IT");
}
@Cacheable
public Employee getEmployeeByNameAndDept(String name, String dept) {
System.out.println("getEmployeeByNameAndDept() invoked");
return new Employee(20, name, dept);
}
}
The main class
public class ExampleMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
EmployeeService employeeService = context.getBean(EmployeeService.class);
System.out.println("-- getting dept list --");
String[] depts = employeeService.getDepartments();
System.out.println("depts returned: "+depts);
System.out.println("-- getting employee by id --");
Employee employee = employeeService.getEmployeeById(10);
System.out.println("employee returned: "+employee);
System.out.println("-- getting employee by name and dept --");
Employee employee2 = employeeService.getEmployeeByNameAndDept("Linda", "Admin");
System.out.println("employee returned: "+employee2);
System.out.println("-- printing native cache --");
CacheManager cm = context.getBean(CacheManager.class);
Cache cache = cm.getCache("employee-cache");
System.out.println(cache.getNativeCache());
}
} -- getting dept list -- getDepartments() invoked depts returned: [Ljava.lang.String;@7fa98a66 -- getting employee by id -- getEmployeeById() invoked employee returned: com.logicbig.example.Employee@15ff3e9e -- getting employee by name and dept -- getEmployeeByNameAndDept() invoked employee returned: com.logicbig.example.Employee@5fdcaa40 -- printing native cache -- {SimpleKey []=[Ljava.lang.String;@7fa98a66, 10=com.logicbig.example.Employee@15ff3e9e, SimpleKey [Linda,Admin]=com.logicbig.example.Employee@5fdcaa40}
As seen in above native cache output, the key generation is according the rules we stated in the beginning of this tutorial.
Generating custom keys
What will happen if we create multiple @Cacheable methods having same argument types? Let's see that by creating two no-args methods:
@Service
@CacheConfig(cacheNames = "employee-cache")
public class EmployeeService2 {
@Cacheable
public String[] getDepartments() {
System.out.println("getDepartments() invoked");
return new String[]{"IT", "Admin", "Account"};
}
@Cacheable
public Employee[] getAllEmployees() {
System.out.println("getAllEmployees() invoked");
return new Employee[]{new Employee(30, "Joe", "Account")};
}
}
public class ExampleMain2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
EmployeeService2 employeeService = context.getBean(EmployeeService2.class);
System.out.println("-- getting dept list --");
String[] depts = employeeService.getDepartments();
System.out.println("depts returned: " + depts);
System.out.println("-- getting all employee list --");
Employee[] allEmployees = employeeService.getAllEmployees();
System.out.println("all employees returned: " + allEmployees);
CacheManager cm = context.getBean(CacheManager.class);
Cache cache = cm.getCache("employee-cache");
System.out.println(cache.getNativeCache());
}
}
-- getting dept list -- getDepartments() invoked depts returned: [Ljava.lang.String;@441772e -- getting all employee list -- java.lang.ClassCastException: [Ljava.lang.String; cannot be cast to [Lcom.logicbig.example.Employee; at com.logicbig.example.EmployeeService2$$EnhancerBySpringCGLIB$$c9867eb7.getAllEmployees(<generated>) at com.logicbig.example.ExampleMain2.main(ExampleMain2.java:18) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.logicbig.invoker.MethodCaller.main(MethodCaller.java:31)
As seen in above output, Spring attempts to retrieved the same cache value of department list (String[]) for the second method. That's because both methods map to the same key value of SimpleKey.EMPTY . To fix this issue we can define a custom key for the second method. @Cacheable method has an element key for that purpose:
@Service
@CacheConfig(cacheNames = "employee-cache")
public class EmployeeService2 {
@Cacheable
public String[] getDepartments() {
System.out.println("getDepartments() invoked");
return new String[]{"IT", "Admin", "Account"};
}
@Cacheable(key = "#root.methodName")
public Employee[] getAllEmployees() {
System.out.println("getAllEmployees() invoked");
return new Employee[]{new Employee(30, "Joe", "Account")};
}
}
@Cacheable#key allows to use Spring Expression Language (SpEL) for computing the key dynamically. Check out this list to understand what expression metadata can be used.
In above example #root.methodName refers to the method which is being invoked.
Let's run ExampleMain2 again:
-- getting dept list -- getDepartments() invoked depts returned: [Ljava.lang.String;@7fa98a66 -- getting all employee list -- getAllEmployees() invoked all employees returned: [Lcom.logicbig.example.Employee;@6cf0e0ba {SimpleKey []=[Ljava.lang.String;@7fa98a66, getAllEmployees=[Lcom.logicbig.example.Employee;@6cf0e0ba}
Also check out this to understand more scenarios where defining custom key generation is desirable.
Example ProjectDependencies and Technologies Used: - spring-context 5.0.4.RELEASE: Spring Context.
- JDK 1.8
- Maven 3.3.9
|