An Example of Aspect-Oriented Programming with Spring


Rob Lambert - 2004 December 01

1. Introduction

I recently wrote an alternate web interface to display music, DVDs and books from Amazon.com. I used the Spring Framework to wire everything together, which has some support for AOP built into it.

AOP (Aspect-Oriented Programming) is a way to modularize code that is typically repeated and sandwiched all over in an object-oriented system. This article describes my first foray into "hands-on" AOP where I actually write a MethodInterceptor of my own. The example described here was my first major AOP "a-ha" moment, and I hope that this will help shed light onto the value of using AOP in applications.

2. The AmazonDataAccessor Interface

The main Java interface that I wrote to get data from Amazon for my site looks something like this:

package com.zabada.amazon;

import com.amazon.xml.AWSECommerceService._CustomerReviews;
import com.amazon.xml.AWSECommerceService._Item;

/**
 * Interface to get items and reviews from Amazon.com
 */
public interface AmazonDataAccessor
{
  /**
   * Get Amazon Item by ASIN (Amazon Standard Identification Number)
   * @param asin
   */
  public _Item getItemByAsin(String asin);
  
  /**
   * Get items by item type and keyword.  ListResults is a simple
   * JavaBean I wrote for this project to hold an _Item array 
   * and other data about the results.
   * @param itemType
   * @param keyword
   * @param page
   */
  public ListResults getItemsByKeyword(ItemType itemType, String keyword, int page);
  
  /**
   * Get items by Amazon browse node.
   * @param browseNode
   * @param page
   */
  public ListResults getItemsByBrowseNode(BrowseNode browseNode, int page);

  /**
   * Get customer reviews by ASIN (Amazon Standard Identification Number)
   * @param asin
   * @param page the page number of the reviews to retrieve
   *        (Amazon returns them in chunks of five)
   */
  public _CustomerReviews getCustomerReviews(String asin, int page);
}

I had written a default implementation of this interface that uses Apache Axis and Java code generated by Axis from the Amazon E-Commerce Service 4.0 WSDL (Web Service Definition Language) to retrieve the Amazon items and reviews. After getting this implementation working, it became clear that it would not be prudent to make the expensive call to the Amazon E-Commerce Service every time I wanted to retrieve something from Amazon; I needed a cache. When a popular item, say a Vanilla Ice CD, is accessed many times in a single day, I'd benefit a lot from a cache. With a cache, I'd use less bandwidth because I wouldn't have to call up Amazon every time a request comes in, Amazon wouldn't hate me for pounding their web services that they have so kindly provided, and if an item has already been accessed, the end user wouldn't have to wait as long for the response.

I had used OSCache before; it was quite simple to use and seemed to work quite well. So I decided that I would use it to cache my Amazon data.

3. Integrating OSCache Into A New Implementation of the AmazonDataAccessor Interface

OSCache is a simple, widely used, high performance J2EE caching framework. An example of basic usage can be read in the JavaDocs for GeneralCacheAdministrator.

Because I thought that I was so clever, I thought I'd use the Decorator design pattern to write an alternate implemention of my AmazonDataAccessor interface. In this implementation I would inject my original implementation into my new alternate implementation. I'd use the original implementation when a queried result was not in the cache and then write that item to the cache so that it would be available in the cache for the next call with identical parameters. Otherwise, I'd just return the value from the cache.

Here is how we could use OSCache "typical use with fail over" to implement the _Item getItemByAsin(String asin) method for our new implementation:

package com.amazon.zabada;

public class CacheEnabledAmazonDataAccessor() implements AmazonDataAccessor
{
  //THE MAIN HANDLE INTO OUR OSCACHE
  private GeneralCacheAdministrator admin = new GeneralCacheAdministrator();

  //THE "DECORATED" ORIGINAL IMPLEMENTATION
  private AmazonDataAccessor ada;

  //THE OSCACHE REFRESH PERIOD IN SECONDS
  private int cacheTimeSeconds = 60*60*24;//1 DAY

  /**
   * Set the time in seconds to cache results
   */
   public void setCacheTimeSeconds(int cacheTimeSeconds)
   {
     this.cacheTimeSeconds = cacheTimeSeconds;
   }

  /**
   * Set the AmazonDataAccessor implementation that this
   * implemention will internally use
   * when a result is not in the cache.
   */
  public void setAmazonDataAccessor(AmazonDataAccessor ada)
  {
    this.ada = ada;
  }

  /**
   * getItemByAsin Implementation that uses OSCache
   * to minimize calls to our "decorated" AmazonDataAccessor implementation
   */
  public _Item getItemByAsin(String asin)
  {
     _Item item = null;
     try
     {
       // Get from the cache, Use ASIN as the cache key
       item = (_Item) admin.getFromCache(asin, cacheTimeSeconds);
     }
     catch (NeedsRefreshException nre)
     {
       // If we get here, the item is not in the cache, or
       // or something bad happened.
       try
       {
         // Get the value by calling the original AmazonDataAccessor
         item = ada.getItemByAsin(asin);
         // Store in the cache
         admin.putInCache(asin, item);
       }
       catch (Exception ex)
       {
         // We have the current content if we want fail-over.
         item = (_Item) nre.getCacheContent();
         // It is essential that cancelUpdate is called if the
         // cached content is not rebuilt
         admin.cancelUpdate(asin);
       }
     }
    }
    return item;
  }
  
  ... 

Cool, I had one method implemented, three more to go... I started implementing the _CustomerReviews getCustomerReviews(String asin, int page) method:

  ...   
  
  /**
   * getCustomerReviews Implementation that uses OSCache
   * to minimize calls to our "decorated" AmazonDataAccessor implementation
   */
  public _CustomerReviews getCustomerReviews(String asin, int page)
  {
     _CustomerReviews reviews = null;
     //Use ASIN and PAGE as the cache key
     String cacheKey = asin+"-"+page;
     try
     {
       // Get from the cache
       reviews = (_CustomerReviews) admin.getFromCache(cacheKey, cacheTimeSeconds);
     }
     catch (NeedsRefreshException nre)
     {
       // If we get here, the item is not in the cache, or
       // or something bad happened.
       try
       {
         // Get the value by calling the original AmazonDataAccessor
         reviews = ada.getCustomerReviews(asin, page);
         // Store in the cache
         admin.putInCache(cacheKey, reviews);
       }
       catch (Exception ex)
       {
         // We have the current content if we want fail-over.
         reviews = (_CustomerReviews) nre.getCacheContent();
         // It is essential that cancelUpdate is called if the
         // cached content is not rebuilt
         admin.cancelUpdate(cacheKey);
       }
     }
    }
    return reviews;
  }

...    
}

Then I started to do the implementation for getItemsByKeyword. "Whoa, wait a minute" I thought to myself. I started to feel a little sick to my stomach because I had just approximately duplicated in getCustomerReviews what I had done for getItemByAsin. Worse yet, I was getting ready to do it again. I could hear in the back of my head "Code duplication is the ultimate code smell. It's a sign that something is very wrong with (your) implementation or design." (words of Rod Johnson from his excellent book Expert One-on-One J2EE Development without EJB). There's got to be a better way.

The problem was that I had scattered code that was very similar in multiple places. This kind of scattering is difficult to modularize, even in object-oriented code. We could push parts of the cache lookup/cache writing/object returning code into helper methods, but even with that, we'd have to make sure to call the helper methods and deal with casting objects to the correct type and things of this nature. Not fun, and definitely error-prone.

4. Writing a MethodInterceptor

Hmmmm. I scratched my head and then it became clear to me that this was a perfect opportunity to write my very first hand-rolled MethodInterceptor to do this nasty work for me.

So I pulled out my copy of J2EEwoEJB and re-read parts of the chapter on AOP, and then rolled up my sleeves and produced the following MethodInterceptor "around" advice. It does all the work for the OSCache "typical use with fail over" generically for any method. Here it is:

package com.zabada.util;

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

import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;

/**
 * MethodInterceptor that looks for requested object in an OSCache
 * and returns it if found, if not found puts the returned object
 * from the interceptee into the OSCache and then returns it.
 * This assumes that the toString().toLowerCase() on each the arguments
 * of the intercepted methods can be considered unique as they are used to
 * to build the keys that will be used to store the Objects in the cache.
 */
public class OSCacheMethodInterceptor implements MethodInterceptor
{
  private int cacheSeconds = 60 * 60 * 24 * 7; //DEFAULT 7 DAYS

  //"saveNullResult" IS A BOOLEAN WHETHER OR NOT TO SAVE THE OBJECT
  //IF THE RETURN VALUE IS NULL. FOR MY USAGE I DO NOT WANT TO SAVE
  //THE NULL RESULT SO WE TRY AGAIN FOR THE NEXT CALL WITH THE SAME PARAMETERS
  private boolean saveNullResult = false;//TODO: MAKE A SETTER FOR THIS
  private static GeneralCacheAdministrator cache = new GeneralCacheAdministrator();

  /**
   * Set time in seconds that returned object will stay active in the cache.
   * The default is 604800 seconds (7 days).
   * @param cacheSeconds
   */
  public void setCacheTimeSeconds(int cacheSeconds)
  {
    this.cacheSeconds = cacheSeconds;
  }

  /**
   * The actual invoke method that intercepts and deals with caching.
   */
  public Object invoke(MethodInvocation invocation) throws Throwable
  {
    //BUILD CACHE KEY USING METHOD NAME AND PARAMETERS.
    //NOTE: THIS CURRENT GENERATION OF A CACHE KEY IS CASE INSENSITIVE
    //AND IS BASED ON THE toString() RESULTS OF THE PARAMETERS.
    //THIS IS OKAY FOR MY USE HERE, BUT TO MAKE THIS MORE GENERIC
    //WE MAY WANT TO PULL THIS FUNCTIONALITY OUT INTO AN INTERFACE SO
    //THAT WE CAN PLUG IN CUSTOM KEY GENERATORS BASED ON THE MethodInvocation
    StringBuffer cacheNameBuffer = new StringBuffer();
    cacheNameBuffer.append(invocation.getMethod().getName());
    if (invocation.getArguments() != null && invocation.getArguments().length > 0)
    {
      for (int a = 0; a < invocation.getArguments().length; a++)
      {
        cacheNameBuffer.append("-");
        if (invocation.getArguments()[a] == null)
          cacheNameBuffer.append("null");
        else
          cacheNameBuffer.append(invocation.getArguments()[a].toString().toLowerCase());
      }
    }
    String cacheKey = cacheNameBuffer.toString();
    try
    {
      /*
      "retrieve from"/"add to" OSCache using GeneralCacheAdministrator
      "typical use with fail over" as outlined in the GeneralCacheAdministrator javadoc
      */
      Object object = null;
      try
      {
        // Get from the cache
        object = cache.getFromCache(cacheKey, cacheSeconds);
      }
      catch (NeedsRefreshException nre)
      {
        //OBJECT NOT IN CACHE
        //!!!! HERE COMES THE MOST IMPORTANT LINE !!!!!
        //PROCEED TO THE ACTUAL METHOD CALL AND GET
        //OBJECT FROM INTERCEPTEE
        object = invocation.proceed();

        //IF saveNullResult SETTING IS FALSE AND OBJECT IS NULL, CANCEL UPDATE OF CACHE
        if(object == null && !saveNullResult)
        {
          //It is essential that cancelUpdate is called if the cached content is not rebuilt
          cache.cancelUpdate(cacheKey);
        }
        else
        {
          try
          {
            cache.putInCache(cacheKey, object);
          }
          catch (Exception ex)
          {
            // We have the current content if we want fail-over.
            object = nre.getCacheContent();
            //It is essential that cancelUpdate is called if cached content is not rebuilt
            cache.cancelUpdate(cacheKey);
          }
        }
      }
      return object;
    }
  }
}

That was a little verbose, but I felt a lot better deleting the CacheEnabledAmazonDataAccessor and just writing the caching code once. Now I've just got to get this thing to actually work ...

5. Integrating the New MethodInterceptor

Now we've got to wire this into our configuration so that all our getter methods on our AmazonDataAccessor implementation get intercepted by OSCacheMethodInterceptor. Originally, the default implementation of AmazonDataAccessor was defined in my Spring configuration file as:

<bean id="amazonDataAccess" class="com.zabada.amazon.AmazonDataAccessorImpl">
  <property name="settings"><ref local="settings"/></property>
</bean>

Here's how I re-wired the configuration to use the new MethodInterceptor:

<!-- DEFINE THE METHOD INTERCEPTOR ADVICE THAT WE JUST WROTE -->
<bean id="osCacheInterceptorAdvice" class="com.zabada.util.OSCacheMethodInterceptor"/>

<!-- DEFINE THE POINTCUT ADVISOR TO INTERCEPT ON GETTER METHODS -->
<bean id="osCacheInterceptor"
      class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
  <property name="advice">
    <ref local="osCacheInterceptorAdvice"/>
  </property>
  <!-- INTERCEPT ON "getter" METHODS -->
  <property name="pattern">
    <value>.*(\.get).*</value>
  </property>
</bean>

<!-- DEFINE OUR ORIGINAL DATA ACCESSOR AS "amazonDataAccessorImpl" -->
<bean id="amazonDataAccessorImpl" class="com.zabada.amazon.AmazonDataAccessorImpl">
  <property name="settings"><ref local="settings"/></property>
</bean>

<!-- OUR NEW BEAN DEFINITION FOR "amazonDataAccessor" THAT WEAVES IT ALL TOGETHER -->
<bean id="amazonDataAccessor" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="proxyInterfaces">
    <value>com.zabada.amazon.AmazonDataAccessor</value>
  </property>
  <!-- DEFINE THE TARGET OF THE PROXY -->
  <property name="target">
    <ref local="amazonDataAccessorImpl"/>
  </property>
  <!-- WIRE IN OUR INTERCEPTOR -->
  <property name="interceptorNames">
    <list>
      <value>osCacheInterceptor</value>
    </list>
  </property>
</bean>

Good. The final thing that we need to do is to make sure that an oscache.properies and the oscache.jar are in the classpath, so I dumped oscache.properties into "WEB-INF/classes" and oscache.jar into "WEB-INF/lib" (download the OSCache distribution here. The full distribution contains example oscache.properties files as well as the JAR).

I rebuilt everything, started up Tomcat and to my amazement, it worked! After a first call to a page, a second call to it was super fast; I could see my disk cache filling up with data!

6. Wrap Up, Conclusion

Fun stuff. Now I we are familar with at least one aspect (no pun intended) of using AOP with Spring. From now on, whenever I find myself using scattered code over and over, I'll know to look into this sort of thing for a solution to make it more modularized. Things like redundant logging, JDBC try/catch/finally stuff and making sure Connections and Streams are closed are great places to do method interception as discussed here.

So try it out, and good luck!

7. Resources

7.1. Source Code and Supporting Libraries

You can download a simple project with source code that shows the OSCacheMethodInterceptor in action here: aop-example.zip. The full Amazon sample is not included here because it would require many more libraries to run and the web services stuff is still a bit ugly so I'd like to clean it up first! This whole Amazon sample project may be a part of a larger series of articles to come later.

7.2. Related Books

Expert One-on-One J2EE Development without EJB by Rod Johnson and Juergen Hoeller.

Spring in Action by Craig Walls, Ryan Breidenbach

Pro Spring by Rob Harrop, Jan Machacek

AspectJ in Action by Ramnivas Laddad

--------------------------------

Rob Lambert is a developer in Chicago working for InterchangeDigital, Inc. Feel free to send feedback to roblambert AT gmail DOT com. I just started a blog over at JRoller, check it out, I may or may not start writing stuff there!

--------------------------------

This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/2.0/ or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.