Rob Lambert - 2004 December 01
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.
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.
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.
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 ...
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!
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!
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.
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
--------------------------------
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.