Wednesday, July 06, 2011

Caching with Spring ehcache and annotations

Update Oct 2012: In fact Spring supports now (JSR-107 AKA JCache although partially as the spec is not still ready as of 2012

Caching is trivial up to the moment you start wanting to cache too many entities. At that point you realize caching is actually a cross cutting concern which basically should be done the easy way, read using Inversion Of Control. But caching is also about where you cache: memory, file system?

If you are using Java then Spring in combination with ehCache can be used to provide caching while abstracting the developer from the details. To be able to achieve caching with minimum effort I recommend using ehcache spring annotations project. Note there is no need to add ehcache dependency as it is added by ehcache-spring-annotations project. Note that Spring 3.1 provides native support so probably it is a better idea to go that route.

Here are the dependencies I used for Spring 3.0.4:
<!-- ehcache -->
        <dependency>
            <groupId>com.googlecode.ehcache-spring-annotations</groupId>
            <artifactId>ehcache-spring-annotations</artifactId>
            <version>1.1.2</version>
            <type>jar</type>
        </dependency>

Create WEB-INF/ehcache.xml with the below content:
<?xml version="1.0" encoding="UTF-8"?>
  <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      <defaultCache eternal="true" maxElementsInMemory="100" overflowToDisk="false" />
      <cache name="findAllClients" maxElementsInMemory="10000" eternal="true" overflowToDisk="false" />
  </ehcache>

In application context add the below lines:
<beans ...xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring"...
...
xsi:schemaLocation="
...
http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd
...
<!-- ehcache -->
    <ehcache:annotation-driven />
 
    <ehcache:config cache-manager="cacheManager">
        <ehcache:evict-expired-elements interval="60" />
    </ehcache:config>
 
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation"  value="/WEB-INF/ehcache.xml"/>
    </bean>
...

Look for a method that returns a collection of entities and annotate it like:
@Cacheable(cacheName = "findAllClients")
    public List<Client> findAll() {

If there is a method that inserts an entity related to the created cache (with name "findAllClients" in this case) then we annotate it so it cleans the cache after insertion:
@TriggersRemove(cacheName = "findAllClients", when = When.AFTER_METHOD_INVOCATION, removeAll = true)
    public void addClient(Client client) {

Of course there are cases where we are just consuming a collection let us say from a web service. We would like to force the cache to be cleaned even though we are never inserting an entity. Here is where you need to use a Controller that can be invoked to clean all or specific caches with a URL like:
http://localhost:8080/ehCache/remove?cacheName=findAllClients&cacheName=anotherCacheToRemove

Here is the Controller that will make this possible:
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/ehCache/*")
public class EhCacheController extends RootController {
    private static final String EHCACHE_SHOW_PATH = "/ehCache/show";
    
    @Autowired
    private CacheManager cacheManager;

    /**
     * Removes all caches if no parameter is passed
     * Removes all caches for the specified "cacheName" parameters
     * 
     * @param request
     * @param response
     * @return
     */
    
    @RequestMapping("/remove")
    public ModelAndView home(HttpServletRequest request, HttpServletResponse response) {
        ControllerContext ctx = new ControllerContext(request, response);
        init(ctx);
        
        String[] storedCacheNames = cacheManager.getCacheNames();
        String[] cacheNames = ctx.getParameterValues("cacheName");
        
        List<Cache> caches = new ArrayList<Cache> (storedCacheNames.length);
        
        for( String storedCacheName : storedCacheNames ){
            Cache storedCache = cacheManager.getCache(storedCacheName);
            if( cacheNames == null ) {
                storedCache.removeAll();
            } else {
                for( String cacheName : cacheNames ) {
                    if( cacheName.equalsIgnoreCase(storedCacheName) ) {
                        storedCache.removeAll();
                    }
                }
            }
            caches.add(storedCache);
        }
        
        ctx.setRequestAttribute("caches", caches);

        return getModelAndView(ctx, EHCACHE_SHOW_PATH);
    }
}

The show.jsp would be something like:
<%@ include file="/WEB-INF/jsp/includes.jsp" %>
<%@ include file="/WEB-INF/jsp/header.jsp" %>
<div class="global_error"><c:out value="${csrfError}" /></div>
<div><c:out value="${caches}" /></div>
<%@ include file="/WEB-INF/jsp/footer.jsp" %>

To add more caching you will need to add the cache entry in the ehcache.xml (thing that I do not like to be honest as I think the annotation should be parsed and if there is no declaration for it in XML then use just the default values or better allow customization as part of the annotation itself) and you need to annotate the method which return value will be cached. To clean that cache you can have either one or a combination of the @TriggersRemove annotation and a URL for cache cleaning like explained before.

Followers