Wednesday, October 17, 2012

@Memoize in Java using Spring and Aspects

Recently, while writing an application in service oriented architecture, I had a couple of places in the code where I needed to make a service call to get the details by a particular application key. I was wondering about duplicate calls, i.e. multiple service calls made to get the details for the same application key. As we all know, service calls are costly with all the marshalling, unmarshalling, cache lookups, database lookups involved, I wanted to cache the results of a service call by application key or keys and this is a clear cross-cutting concern. Having programmed in Perl and Python that have the memoize option, I thought a Memoize aspect would be most appropriate here. As always, I was looking for Spring's Aspect Oriented Programming APIs and though it was tricky to get the set up right, I ended up with a very small amount of code that does the basic memoization. Since the set up was tricky, I thought I would document here for reference:


  1. Set up Spring, AspectJ, CGLib and ASM for the application.
    1. The following Spring jars are needed:
      1. spring-aop (v3.3.1)
      2. spring-aspects (v3.3.1)
      3. spring-beans (v3.3.1)
      4. spring-context (v3.3.1)
      5. spring-context-support (v3.3.1)
      6. spring-core (v3.3.1)
      7. spring-test (v3.3.1)
      8. spring-expression (v3.3.1)
      9. spring-asm (v3.3.1)
    2. The following AspectJ jars are needed for load-time-weaving and aspectj runtime.
      1. aspectj-rt (v1.6.9)
      2. aspectj-weaver (v1.5.4)
      3. aop-alliance (v1.0)
    3. The following CGLib jars are needed for code generation
      1. cglib (v2.2.3)
    4. The following ASM jars are needed for byte-code manipulation
      1. asm (v3.3.1)
    5. Commons logging jar needed by Spring
      1. commons-logging (v1.1.1)
  2. The CGLib and ASM libraries should be compatible with each other. So, if you choose a different version of any of these jars, make sure to select the compatible other one. Otherwise, you will end up with a runtime error (NoSuchMethodError).
  3. Enable aspects in your spring application using the following configuration:
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
                ">
    
        <context:component-scan base-package="com.vikdor.aspects" />
        <aop:aspectj-autoproxy proxy-target-class="true" />
    </beans>
    
  4. Define the annotation that is going to be used in the advice:
    import java.lang.annotation.Retention;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Memoize
    {
    
    }
    
  5. Write a simple Aspect class, annotate it with @Aspect and define an @Around advice for all the methods that use the @Memoize annotation, as follows:
    @Aspect
    @Component
    public class MemoizeAspect
    {
        private WeakHashMap<Object[], Object> cache = new WeakHashMap<Object[], Object>();
    
        @Around("@annotation(com.vikdor.aspects.Memoize)")
        public Object handledMemoize(ProceedingJoinPoint pjp) throws Throwable
        {
            System.out.println("Handling memoize");
            Object[] args = pjp.getArgs();
    
            if (args != null)
            {
                Object response = cache.get(Arrays.asList(args));
                if (response != null)
                {
                    return response;
                }
            }
            Object obj = pjp.proceed();
            if (args != null)
            {
                cache.put(Arrays.asList(args), obj);
            }
            return obj;
        }
    }
    
Now, add @Memoize to any method whose request and response should be cached and it just works. BTW, this is a very naive Memoize implementation and is not production ready at all ;-)

2 comments:

  1. i really like that you are giving information on core and advance java concepts. Being enrolled at http://www.wiziq.com/course/1779-core-and-advance-java-concepts i found your information very helpful indeed.thanks for it.

    ReplyDelete
  2. This is a nice and simple demonstration of a memoization aspect. "It just works" is dependent on a few simplifying assumptions:
    1. The args list is good as hashmap keys. That requires the List to have hashcode() and equals() that are based on the elements. All the method args, therefore, must have state-based hashcode() and equals().
    2. The args must be immutable - they must stay unchanged over time and across threads.
    3. The method to be memoized must not read or write any instance variables the object. Otherwise the memoized results are wrong or stale.

    ReplyDelete