Saturday, 8 September 2012

Spring 3.1 Caching and @Cacheable

Caches have been around in the software world for long time. They’re one of those really useful things that once you start using them you wonder how on earth you got along without them so, it seems a little strange that the Guys at Spring only got around to adding a caching implementation to Spring core in version 3.1. I’m guessing that previously it wasn’t seen as a priority and besides, before the introduction of Java annotations, one of the difficulties of caching was the coupling of caching code with your business code, which could often become pretty messy.

However, the Guys at Spring have now devised a simple to use caching system based around a couple of annotations: @Cacheable and @CacheEvict.

The idea of the @Cacheable annotation is that you use it to mark the method return values that will be stored in the cache.
The @Cacheable annotation can be applied either at method or type level. When applied at method level, then the annotated method’s return value is cached. When applied at type level, then the return value of every method is cached.

The code below demonstrates how to apply @Cacheable at type level:

@Cacheable(value = "employee")
public class EmployeeDAO {

 
public Person findEmployee(String firstName, String surname, int age) {

   
return new Person(firstName, surname, age);
 
}

 
public Person findAnotherEmployee(String firstName, String surname, int age) {

   
return new Person(firstName, surname, age);
 
}
}

The Cacheable annotation takes three arguments: value, which is mandatory, together with key and condition. The first of these, value, is used to specify the name of the cache (or caches) in which the a method’s return value is stored.

  @Cacheable(value = "employee")
 
public Person findEmployee(String firstName, String surname, int age) {

   
return new Person(firstName, surname, age);
 
}

The code above ensures that the new Person object is stored in the “employee” cache.

Any data stored in a cache requires a key for its speedy retrieval. Spring, by default, creates caching keys using the annotated method’s signature as demonstrated by the code above. You can override this using @Cacheable’s second parameter: key. To define a custom key you use a SpEL expression.

  @Cacheable(value = "employee", key = "#surname")
 
public Person findEmployeeBySurname(String firstName, String surname, int age) {

   
return new Person(firstName, surname, age);
 
}

In the findEmployeeBySurname(...) code, the ‘#surname’ string is a SpEL expression that means ‘go and create a key using the surname argument of the findEmployeeBySurname(...) method’.

The final @Cacheable argument is the optional condition argument. Again, this references a SpEL expression, but this time it’s specifies a condition that’s used to determine whether or not your method’s return value is added to the cache.

  @Cacheable(value = "employee", condition = "#age < 25")
 
public Person findEmployeeByAge(String firstName, String surname, int age) {

   
return new Person(firstName, surname, age);
 
}

In the code above, I’ve applied the ludicrous business rule of only caching Person objects if the employee is less than 25 years old.

Having quickly demonstrated how to apply some caching, the next thing to do is to take a look at what it all means.

  @Test
 
public void testCache() {

   
Person employee1 = instance.findEmployee("John", "Smith", 22);
    Person employee2 = instance.findEmployee
("John", "Smith", 22);

    assertEquals
(employee1, employee2);
 
}

The above test demonstrates caching at its simplest. The first call to findEmployee(...), the result isn’t yet cached so my code will be called and Spring will store its return value in the cache. In the second call to findEmployee(...) my code isn’t called and Spring returns the cached value; hence the local variable employee1 refers to the same object reference as employee2, which means that the following is true:

    assertEquals(employee1, employee2);

But, things aren’t always so clear cut. Remember that in findEmployeeBySurname I’ve modified the caching key so that the surname argument is used to create the key and the thing to watch out for when creating your own keying algorithm is to ensure that any key refers to a unique object.

  @Test
 
public void testCacheOnSurnameAsKey() {

   
Person employee1 = instance.findEmployeeBySurname("John", "Smith", 22);
    Person employee2 = instance.findEmployeeBySurname
("Jack", "Smith", 55);

    assertEquals
(employee1, employee2);
 
}

The code above finds two Person instances which are clearly refer to different employees; however, because I’m caching on surname only, Spring will return a reference to the object that’s created during my first call to findEmployeeBySurname(...). This isn’t a problem with Spring, but with my poor cache key definition.

Similar care has to be taken when referring to objects created by methods that have a condition applied to the @Cachable annotation. In my sample code I’ve applied the arbitrary condition of only caching Person instances where the employee is under 25 years old.

  @Test
 
public void testCacheWithAgeAsCondition() {

   
Person employee1 = instance.findEmployeeByAge("John", "Smith", 22);
    Person employee2 = instance.findEmployeeByAge
("John", "Smith", 22);

    assertEquals
(employee1, employee2);
 
}

In the above code, the references to employee1 and employee2 are equal because in the second call to findEmployeeByAge(...) Spring returns its cached instance.

  @Test
 
public void testCacheWithAgeAsCondition2() {

   
Person employee1 = instance.findEmployeeByAge("John", "Smith", 30);
    Person employee2 = instance.findEmployeeByAge
("John", "Smith", 30);

    assertFalse
(employee1 == employee2);
 
}

Similarly, in the unit test code above, the references to employee1 and employee2 refer to different objects as, in this case, John Smith is over 25.

That just about covers @Cacheable, but what about @CacheEvict and clearing items form the cache? Also, there’s the question adding caching to your Spring config and choosing a suitable caching implementation. However, more on that later....

6 comments:

Christian Schlichtherle said...

You need to use assertSame, otherwise the code depends on .equals(Object), which hasn't been shown. It may have been overridden to compare all properties and then this test would always pass, even if nothing was cached.

Roger Hughes said...

Christian
You're quite right, this is something I spotted after I'd published the blog. In my simple example I'm not overriding the equals() method; hence, the code works as it stands because Object.equals() simply performs an '==' comparison.

Tim said...

Hi Roger.

I have some troubles with this annotation. I think i done all properly, but i getting "cache miss" all the time. It seams that no cache at all.
Ehcache config is ok. I see log lines that it starts and i see books.data (my cache name) in temp directory, but size of this file is 0.
No error at runtime.
I turn on ehcache log leve to all, and see only this messages at starttime:

2874 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.config.ConfigurationHelper - No CacheExceptionHandlerFactory class specified. Skipping...
2894 [RMI TCP Connection(2)-127.0.0.1] DEBUG net.sf.ehcache.store.MemoryStore - Initialized net.sf.ehcache.store.MemoryStore for books
2903 [RMI TCP Connection(2)-127.0.0.1] DEBUG net.sf.ehcache.DiskStorePathManager - Using diskstore path /Library/Tomcat/temp
2903 [RMI TCP Connection(2)-127.0.0.1] DEBUG net.sf.ehcache.DiskStorePathManager - Holding exclusive lock on /Library/Tomcat/temp/.ehcache-diskstore.lock
2903 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.store.disk.DiskStorageFactory - Failed to delete file books.data
2903 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.store.disk.DiskStorageFactory - Failed to delete file books.index
2928 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.store.disk.DiskStorageFactory - Matching data file missing (or empty) for index file. Deleting index file /Library/Tomcat/temp/books.index
2928 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.store.disk.DiskStorageFactory - Failed to delete file books.index
2938 [RMI TCP Connection(2)-127.0.0.1] DEBUG net.sf.ehcache.Cache - Initialised cache: books
2938 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.config.ConfigurationHelper - CacheDecoratorFactory not configured. Skipping for 'books'.
2939 [RMI TCP Connection(2)-127.0.0.1] DEBUG n.s.e.config.ConfigurationHelper - CacheDecoratorFactory not configured for defaultCache. Skipping for 'books'.

Finally, as i thought i must have some kind of proxy of my Service class, but i have my own implementation. Is it ok? It works different way?
Any ideas?

Roger Hughes said...

Tim
Not sure what's going wrong without looking at the code. You can see from the log that your initialising a cache called 'books', which should match up with the value attribute of the @Cacheable annotation. It must be down to config, so if you haven't already, take a look at:

/2012/09/spring-31-caching-and-config.html#.UmmJgTK9KK0

Tim said...

Roger

I found error source:

only looks for @Cacheable/@CacheEvict on beans in the same application context it is defined in. This means that, if you put in a WebApplicationContext for a DispatcherServlet, it only checks for @Cacheable/@CacheEvict beans in your controllers, and not your services.

I moved cache config to my spring-mvc-config and cahce starts work. To be honest i do not understand how it works.

Roger Hughes said...

Tim
Thanks for the comment, it's a good point, though not unexpected and not an error. ApplicationContexts, as far as I'm aware don't talk to one another, they're independent, decoupled entities. The clue is in the name ApplicationContext, i.e. there's usually only one per application. As you know, you define an ApplicationContext using the Spring config XML file. If you need to combine these files to create a larger context then you can use the import directive. For example importing a database context into your main context by doing something like this:

<beans:import resource="spring-datasource.xml" />