Saturday, June 23, 2007

Caching methods result using Spring and EHCache

Introduction


Spring1.1.1 introduced integration for EHCache for general cache use.
I will show here a sample Interceptor that allows for declarative caching of methods results.

Configure EHCache using Spring IoC


Spring makes EHCache configuration very easy. All you need, is to provide a ehcache.xml file where you configure EHCache regions:


<ehcache>

    <!-- Sets the path to the directory where cache .data files are created.

         If the path is a Java System Property it is replaced by
         its value in the running VM.

         The following properties are translated:
         user.home - User's home directory
         user.dir - User's current working directory
         java.io.tmpdir - Default temp file path -->
    <diskStore path="java.io.tmpdir"/>


    <!--Default Cache configuration. These will applied to caches programmatically created through
        the CacheManager.

        The following attributes are required for defaultCache:

        maxInMemory       - Sets the maximum number of objects that will be created in memory
        eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                            is never expired.
        timeToIdleSeconds - Sets the time to idle for an element before it expires.
                            i.e. The maximum amount of time between accesses before an element expires
                            Is only used if the element is not eternal.
                            Optional attribute. A value of 0 means that an Element can idle for infinity
        timeToLiveSeconds - Sets the time to live for an element before it expires.
                            i.e. The maximum time between creation time and when an element expires.
                            Is only used if the element is not eternal.
        overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                            has reached the maxInMemory limit.

        -->

    <cache name="org.sample.cache.METHOD_CACHE"
        maxElementsInMemory="300"
        eternal="false"
        timeToIdleSeconds="500"
        timeToLiveSeconds="500"
        overflowToDisk="true"
        />
</ehcache>


Our Interceptor will use region "org.sample.cache.METHOD_CACHE" to cache methods results.
Now we will use some Spring IoC to make this region accessible to our beans:

<!-- ==============================   CACHE   ============================= -->

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
  <property name="configLocation">
    <value>classpath:ehcache.xml</value>
  </property>
</bean>

<bean id="methodCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
  <property name="cacheManager">
    <ref local="cacheManager"/>
  </property>
  <property name="cacheName">
    <value>org.sample.cache.METHOD_CACHE</value>
  </property>
</bean>

Bean methodCache creates cache region org.sample.cache.METHOD_CACHE.

Creating our MethodCacheInterceptor


The interceptor implements org.aopalliance.intercept.MethodInterceptor. Whenever it kicks-in, it first checks if the intercepted method is configured to be cached. This allows to selectively configure bean methods for caching. If the method call is configured for caching, the interceptor builds the cache key for the method and checks if the method result is in the cache. If so, the cached result is returned, otherwise the intercepted method is called and the result cached for further use.
/*
 * Copyright 2002-2004 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.sample.interceptor;

import java.io.Serializable;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

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

/**
 */
public class MethodCacheInterceptor implements MethodInterceptor, InitializingBean {
  private static final Log logger = LogFactory.getLog(MethodCacheInterceptor.class);

  private Cache cache;

  /**
   * sets cache name to be used
   */
  public void setCache(Cache cache) {
    this.cache = cache;
  }

  /**
   * Checks if required attributes are provided.
   */
  public void afterPropertiesSet() throws Exception {
    Assert.notNull(cache, "A cache is required. Use setCache(Cache) to provide one.");
  }

  /**
   * main method
   * caches method result if method is configured for caching
   * method results must be serializable
   */
  public Object invoke(MethodInvocation invocation) throws Throwable {
    String targetName  = invocation.getThis().getClass().getName();
    String methodName  = invocation.getMethod().getName();
    Object[] arguments = invocation.getArguments();
    Object result;

    logger.debug("looking for method result in cache");
    String cacheKey = getCacheKey(targetName, methodName, arguments);
    Element element = cache.get(cacheKey);
    if (element == null) {
      //call target/sub-interceptor
      logger.debug("calling intercepted method");
      result = invocation.proceed();

      //cache method result
      logger.debug("caching result");
      element = new Element(cacheKey, (Serializable) result);
      cache.put(element);
    }
    return element.getValue();
  }

  /**
   * creates cache key: targetName.methodName.argument0.argument1...
   */
  private String getCacheKey(String targetName,
                             String methodName,
                             Object[] arguments) {
    StringBuffer sb = new StringBuffer();
    sb.append(targetName)
      .append(".").append(methodName);
    if ((arguments != null) && (arguments.length != 0)) {
      for (int i=0; i < arguments.length; i++) {
        sb.append(".")
          .append(arguments[i]);
      }
    }

    return sb.toString();
  }
}

MethodCacheInterceptor source shows that:

  • by default, all methods result are cached (methodNames is null)

  • cache region is configured using IoC

  • cacheKey takes in consideration method arguments


Using MethodCacheInterceptor



The following excerpt shows how to configure MethodCacheInterceptor:

<bean id="methodCacheInterceptor" class="org.sample.interceptor.MethodCacheInterceptor">
  <property name="cache">
    <ref local="methodCache" />
  </property>
</bean>

<bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
  <property name="advice">
    <ref local="methodCacheInterceptor"/>
  </property>
  <property name="patterns">
    <list>
      <value>.*methodOne</value>
      <value>.*methodTwo</value>
    </list>
  </property>
</bean>

<bean id="myBean" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="target">
   <bean class="org.sample.beans.MyBean"/>
  </property>
  <property name="interceptorNames">
    <list>
      <value>methodCachePointCut</value>
    </list>
  </property>
</bean>


Further improvements:



It will be very helpfull to add the following to MethodCacheInterceptor:

  • refactor the code so that the interceptor no longer depends on EHCache


  • add arguments to methods configuration:
    
    <property name="methodNames">
      <list>
        <value>methodOne(java.lang.String, int)</value>
        <value>methodOne(java.lang.String, java.lang.String)</value>
      </list>
    </property>
    
    

  • add regular expressions to method configuration
    
    <property name="methodNames">
      <list>
        <value>add*</value>
      </list>
    </property>
    
    


By O. Irbouh

4 comments:

Gurpreet said...

Can you please explain the class used : org.sample.beans.MyBean
What actually is this class ?

Anonymous said...

When I tried ti use this example the following exception is generated:

008-07-18 15:22:06,821 [Thread-282] ERROR org.springframework.ws.transport.http.MessageDispatcherServlet - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'methodCacheInterceptor' defined in ServletContext resource [/WEB-INF/spring-ws-servlet.xml]: Initialization of bean failed; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert property value of type [net.sf.ehcache.Cache] to required type [net.sf.ehcache.Cache] for property 'cache'; nested exception is java.lang.IllegalArgumentException: Cannot convert value of type [net.sf.ehcache.Cache] to required type [net.sf.ehcache.Cache] for property 'cache': no matching editors or conversion strategy found
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:480)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:409)
at java.security.AccessController.doPrivileged(AccessController.java:214)

Vikrant Korde said...

This seems to be a problem of duplicate Jars but which are those jars, is not clear.
I am also looking for that.
for details see this
http://forum.springframework.org/showthread.php?t=57638

Vikrant Korde said...

I found that I was having ehcache-1.2.2.jar and ehcache-1.jar in lib folder.
I kept only ehcache-1.2.2.jar file in lib.