Coupling Spring and EHCache using annotations : how-to ?

Hi reader,

I’m willing to share my few knownledge about caching. But before exploring any solution one must be aware of the benefit of Caching.
Why do people use caching techniques ? Well I’m not an expert but I do want my apps to be highly responsive. And I know caching is one of the techniques wich will help my system get to that result.

Some years ago caching was a home-made and craft design. It was often buggy because many concepts were mixed. The responsibility were not identified correctly. People were not familiar with AOP. Well, I was not at least.
Those features are expected from a caching system :
– declarative and programmatic configuration.
– a way to specify eviction policies (LRU, LFU, FIFO)
– a way to create, update, delete and read an entry in the cache.
– a way to specify behaviour at method level (annotations)
– a way to specify overflow strategies (overflow to disk or not)

Some advanced features such as cluster can be of interest but are beyond the scope of the article.

Hibernate distribution was shipped with EhCache. It was an armless piece of soft because it was silent. One day I gave it a try.
At that time it was some xml configuration. No spring coupling, nothing.

Then Spring created spring-modules, a spring project which aimed to provide support and integration for many technologies : caching solutions, mvc addons, web template engines, etc. That project activity started to slow down, thus becoming a risky framework to choose.

I started with spring-modules, then due to the risky part of the activity I long looked for a spring-ehcache integration equivalent. I recently found out ehcache-spring-annotations project. It is the exact project I was looking for. It is googlecode hosted, can be found in maven public repository (which is quiet convenient), is very active and documented enough.

To achive caching strategy one must :

– configure project to use ehcache-spring-annotations and ehcache (shipped with ehcache-spring-annotations)

			<dependency>
				<groupId>com.googlecode.ehcache-spring-annotations</groupId>
				<artifactId>ehcache-spring-annotations</artifactId>
				<version>${ehcache-spring-annotations.version}</version>
			</dependency>

– configure project to use spring-context-support


            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context-support</artifactId>
                <version>${spring.version}</version>
            </dependency>


– design a test : ehcache CacheManager behaviour is tested. Some behaviours can be suprising and I was glad I wrote test fixtures to look into the cache and test cache size.

package org.diveintojee.poc.ehcache.spring.business;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.Collection;
import java.util.List;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

import org.apache.commons.lang.StringUtils;
import org.diveintojee.poc.ehcache.spring.domain.Product;
import org.diveintojee.poc.ehcache.spring.domain.services.ProductService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @author louis.gueye@gmail.com
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {Constants.TESTS_CONTEXT})
public class ProductServiceTest {

   private static final int PRODUCTS_INITIAL_SIZE = 5;
   @Autowired
   private ProductService productService;

   @Autowired
   private CacheManager cacheManager;

   /**
    *
    */
   @Before
   public void before() {

      assertNotNull(productService);

      assertNotNull(cacheManager);

      setUpNewProductRepository();

      cacheManager.getCache(ProductService.PRODUCT_CACHE_NAME).flush();

   }

   /**
    *
    */
   @Test
   public void testList() {

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

      List products = productService.list();

      assertNotNull(products);

      assertEquals(PRODUCTS_INITIAL_SIZE, products.size());

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 5);

   }

   /**
    *
    */
   @Test
   public void testFindByDescription() {

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

      Product p = new Product();

      String name = "doliprane";

      String description = "Ce médicament est un antalgique et un antipyrétique qui contient du paracétamol."
            + "\nIl est utilisé pour faire baisser la fièvre et dans le traitement des affections douloureuses.";

      p.setName(name);

      p.setDescription(description);

      productService.add(p);

      assertEquals(PRODUCTS_INITIAL_SIZE + 1, productService.list().size());

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, PRODUCTS_INITIAL_SIZE + 1);

      String term = "description";

      assertEquals(PRODUCTS_INITIAL_SIZE, productService.findByDescription(term).size());

   }

   /**
    *
    */
   @Test
   public void testAdd() {

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

      Product product = new Product();

      String name = "doliprane";

      String description = "Ce médicament est un antalgique et un antipyrétique qui contient du paracétamol."
            + "\nIl est utilisé pour faire baisser la fièvre et dans le traitement des affections douloureuses.";

      product.setName(name);

      product.setDescription(description);

      Long productId = productService.add(product);

      assertNotNull(productService.get(productId));

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 1);

      assertCacheContains(ProductService.PRODUCT_CACHE_NAME, product);
   }

   /**
    *
    */
   @Test
   public void testUpdate() {

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

      Product product = new Product();

      String name = "doliprane";

      String description = "Ce médicament est un antalgique et un antipyrétique qui contient du paracétamol."
            + "\nIl est utilisé pour faire baisser la fièvre et dans le traitement des affections douloureuses.";

      product.setName(name);

      product.setDescription(description);

      Long productId = productService.add(product);

      product = productService.get(productId);

      assertNotNull(product);

      String newName = "prozac";

      product.setName(newName);

      String newDescription = "Ce médicament est un antidépresseur de la famille des inhibiteurs de la recapture de la sérotonine.";

      newDescription += "Il est utilisé chez l'adulte dans le traitement : -des états dépressifs ; \n-des troubles obsessionnels compulsifs ;";

      newDescription += "\n- de la boulimie (en complément d'une psychothérapie). ;";

      product.setDescription(newDescription);

      productService.update(product);

      product = productService.get(productId);

      assertEquals(newDescription, product.getDescription());

      assertEquals(newName, product.getName());

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 1);

      assertCacheContains(ProductService.PRODUCT_CACHE_NAME, product);

   }

   /**
    *
    */
   @Test
   public void testGet() {

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

      Product product = new Product();

      String name = "doliprane";

      String description = "Ce médicament est un antalgique et un antipyrétique qui contient du paracétamol."
            + "\nIl est utilisé pour faire baisser la fièvre et dans le traitement des affections douloureuses.";

      product.setName(name);

      product.setDescription(description);

      Long productId = productService.add(product);

      product = productService.get(productId);

      assertNotNull(product);

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 1);

      assertCacheContains(ProductService.PRODUCT_CACHE_NAME, product);

   }

   /**
    *
    */
   @Test
   public void testDelete() {

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

      Product product = new Product();

      String name = "doliprane";

      String description = "Ce médicament est un antalgique et un antipyrétique qui contient du paracétamol."
            + "\nIl est utilisé pour faire baisser la fièvre et dans le traitement des affections douloureuses.";

      product.setName(name);

      product.setDescription(description);

      Long productId = productService.add(product);

      assertNotNull(productService.get(productId));

      productService.delete(productId);

      assertNull(productService.get(productId));

      assertCacheSize(ProductService.PRODUCT_CACHE_NAME, 0);

   }

   /**
    *
    */
   private void setUpNewProductRepository() {

      productService.clear();

      for (int i = 0; i < PRODUCTS_INITIAL_SIZE; i++) {          Product p = new Product();          String name = "name" + i;          String description = "description" + i;          p.setName(name);          p.setDescription(description);          productService.add(p);       }    }    @SuppressWarnings("unchecked")    private  void assertCacheSize(String cacheName, int expectedSize) {       if (StringUtils.isEmpty(cacheName))          throw new IllegalArgumentException("Cache name is required");       Cache cache = cacheManager.getCache(cacheName);       assertNotNull(cache);       assertNotNull(cache.getKeys());       if (cache.getKeys().size() == 0) {          assertEquals(0, expectedSize);          return;       }       assertNotNull(cache.getKeys().get(0));       Element element = cache.get(cache.getKeys().get(0));       assertNotNull(element);       if (element.getValue() == null) {          assertEquals(0, expectedSize);          return;       }       assertNotNull(element.getValue());       if (expectedSize == 1) {          assertTrue(!(element.getValue() instanceof Collection));          return;       }       List fromCache = (List) element.getValue();       assertEquals(expectedSize, fromCache.size());    }    @SuppressWarnings("unchecked")    private  void assertCacheContains(String cacheName, T obj) {       if (StringUtils.isEmpty(cacheName))          throw new IllegalArgumentException("Cache name is required");       if (obj == null)          throw new IllegalArgumentException("Not null object is required");       Cache cache = cacheManager.getCache(cacheName);       assertNotNull(cache);       assertNotNull(cache.getKeys());       assertNotNull(cache.getKeys().get(0));       Element element = cache.get(cache.getKeys().get(0));       assertNotNull(element);       assertNotNull(element.getValue());       if (element.getValue() instanceof List) {          List fromCache = (List) element.getValue();          assertNotNull(fromCache);          assertTrue(fromCache.size() > 0);

         assertTrue(fromCache.contains(obj));

      } else {

         assertEquals(obj, element.getValue());

      }

   }
}

– design a domain

/**
 *
 */
package org.diveintojee.poc.ehcache.spring.domain;

/**
 * @author louis.gueye@gmail.com
 */
public class Product extends AbstractPersistableEntity {

   /**
    *
    */
   private static final long serialVersionUID = 6117126134583209714L;

   private String name;

   private String description;

   /**
    * @return
    */
   public String getName() {
      return name;
   }

   /**
    * @param name
    */
   public void setName(String name) {
      this.name = name;
   }

   /**
    * @return
    */
   public String getDescription() {
      return description;
   }

   /**
    * @param description
    */
   public void setDescription(String description) {
      this.description = description;
   }

}

– design a service and annotate the service with expected caching behaviours (@Cacheable for reading operations and @TriggersRemove for update operations)

/**
 *
 */
package org.diveintojee.poc.ehcache.spring.domain.services;

import java.util.List;

import javax.jws.WebService;

import org.diveintojee.poc.ehcache.spring.domain.Product;

import com.googlecode.ehcache.annotations.Cacheable;
import com.googlecode.ehcache.annotations.TriggersRemove;
import com.googlecode.ehcache.annotations.When;

/**
 * @author louis.gueye@gmail.com
 */
@WebService
public interface ProductService {

   String BEAN_ID = "productService";

   String PRODUCT_CACHE_NAME = "product-cache";

   String WEBSERVICE_ENDPOINT_INTERFACE = "org.diveintojee.poc.ehcache.spring.domain.services.ProductService";

   /**
    * @return
    */
   @Cacheable(cacheName = ProductService.PRODUCT_CACHE_NAME)
   List list();

   /**
    * @param term
    * @return
    */
   List findByDescription(String term);

   /**
    * @param product
    * @return
    */
   @TriggersRemove(cacheName = ProductService.PRODUCT_CACHE_NAME, removeAll = true, when = When.AFTER_METHOD_INVOCATION)
   Long add(Product product);

   /**
    * @param product
    */
   @TriggersRemove(cacheName = ProductService.PRODUCT_CACHE_NAME, removeAll = true, when = When.AFTER_METHOD_INVOCATION)
   void update(Product product);

   /**
    * @param id
    */
   @TriggersRemove(cacheName = ProductService.PRODUCT_CACHE_NAME, removeAll = true, when = When.AFTER_METHOD_INVOCATION)
   void delete(Long id);

   /**
    * @param product
    * @return
    */
   @Cacheable(cacheName = ProductService.PRODUCT_CACHE_NAME)
   Product get(Long id);

   /**
	 *
	 */
   @TriggersRemove(cacheName = ProductService.PRODUCT_CACHE_NAME, removeAll = true, when = When.AFTER_METHOD_INVOCATION)
   void clear();

}

– design the service implementation. The nice feature of ehcache-spring-annotations is the ability to annotate at the interface level. I find it more elegant to express things at interface level : it communicates the intent. Other would say that caching is a technical feature and thus should stay hidden in the implementation. The goods news is : whatever flavour is available interface or implementation level.
The implementation doesn’t have anything to do with caching so nothing interesting here.

– configure ehcache to meet caching behaviours expectations.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
	updateCheck="false">

	<diskStore path="java.io.tmpdir/ehcache-spring-poc-server" />

	<defaultCache eternal="false" maxElementsInMemory="1000"
		overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
		timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />

	<cache name="product-cache" eternal="false" maxElementsInMemory="100"
		overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
		timeToLiveSeconds="300" memoryStoreEvictionPolicy="LRU" />

</ehcache>

Hope this article was helpful to anyone willing to integrate EhCache and Spring in an elegant and non-intrusive way.

The complete source code is available here.

The ehcache-spring-annotations documentation was really helpfull and is quiet complete.

2 thoughts on “Coupling Spring and EHCache using annotations : how-to ?

Leave a comment