Ghost in the JVM

An adventure in the JVM land

Java Proxy Chaining - Aspect on the Cheap

How many of you have written a caching solution for one of your expensive method calls? Or how about retry logic for your service calls? Living in such a connected world no single application is its own island. For every applications I have worked for, there always external dependencies, either other service APIs or databases that we need to rely upon. And we can’t make the assumption that they are reliable 100% either.

Some service calls are expensive and the underlying data don’t change so often. We tend to cache the results. A naive approach would be to find such methods and refactor them so that cache can be used before the call to the services are made. The better way is to use AOP where you intercept those calls with ‘aspects’. The in those aspects you can make the decision to whether get the results from the cache or to make the expensive service calls. Another common scenario is retry when you encounter exceptions in your service calls: transient network issues (latency, timed out, spillover in load balancer…) or database hiccups. Normally, you should at least retry the calls for several times before giving up. Like noted previously, a naive approach would be to go every methods and apply the retry logic. Or you could use AOP.

In this post, I’m going to talk about how to use aspect oriented way to ease the refactoring effort. I will not talk about the full blown bytecode level AOP solution which uses AspectJ with bytecode weaving. Instead, I will talk about a lighter weight of aspect programming using the Java’s dynamic proxy and its reflection mechanism. I think it’s pretty similar to the way Spring AOP works. The only difference is that my code will assume every method calls implement interfaces. Thus, it will not have to use cglib to generate the proxies. Also, I think programming to interface is a much cleaner and prefer way for your service calls Data access objects (DAO).

At the end you could decorate your method with annotations/aspects like this:

Example
1
2
3
4
5
6
7
@Timeit
@Retry(times=3)
@Cache(timeToLiveInSeconds=3600)
public void goGetMyData(String someParam, int anotherOne)
{
    // do something
}

Let’s define an example interface for the DAO and its implementation.

Interface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Say
{
    public void say();
}

public void say()
{
    Random rand = new Random();
  int r = rand.nextInt(9);
  if (r > 7)
  {
      throw new RuntimeException("CRAP please retry!!");
  }
  
    System.out.println("Say hello");
}

I intentionally throw a RuntimeException 30% of the times this method run to simulation transient error that could be retried. Now is the fun part: we will add additional functionalities over this method without modifying its code. As the begining I want to time the method performance and retry if it fails (up to 3 times before I give up).

The easiest way to do this is to use annotation to denote your new aspects.

Timing Aspect Annotation
1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Timeit
{
}

And the Retry aspect with maximum of 3 retries before giving up:

Retry Aspect
1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Retry
{
    int times() default 3;
}

In order to facilitate the annotations in the java dynamic proxy, we need to create an InvocationHandler for each of those annotations. For this I first borrow the utility class from “Java Reflection in Action”. You can get full source at the end of this post. I then create a base Interceptor on top of this invocation handler to make the dynamic proxy generation handling annotations easier.

AbstractInvocationHandler and The base Invoker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public abstract class AbstractInvocationHandler<T> implements InvocationHandler
{

    protected T nextTarget;
    protected T realTarget = null;

    public AbstractInvocationHandler(T target)
    {
        nextTarget = target;
        if (nextTarget != null)
        {
            realTarget = findRealTarget(nextTarget);
            if (realTarget == null)
                throw new RuntimeException("findRealTarget failure");
        }
    }
    ...
}

interface RealInvoker
{
    public Object invoke() throws Throwable;
}

interface Invoker<A extends Annotation>
{
  public Object execute(final Method method, final Object[] args, A ann, RealInvoker realInvoker) throws Throwable;
}

class Interceptor<T, A extends Annotation> extends AbstractInvocationHandler<T>
{
    ...
  
    public static <T, A extends Annotation> T createProxy(T obj, Class<A> annotationClass, Invoker<A> invoker)
    {
        return (T) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                .getClass().getInterfaces(), new Interceptor(obj, annotationClass, invoker));
    }

  @Override
  public Object invoke(Object proxy, final Method method, final Object[] args)
          throws Throwable {
      Object result = null;
      A annotation = (A) this.realTarget.getClass().getMethod(method.getName()).getAnnotation(annotationClass);
        if (annotation != null)
        {
          return invoker.execute(method, args, annotation, new RealInvoker() {
              @Override
              public Object invoke() throws Throwable {
                  return method.invoke(Interceptor.this.nextTarget, args);
              }
          });
        }
        else
        {
            result = method.invoke(this.nextTarget, args);
        }

        return result;
  }
}

The nice thing in doing this is that in order to create an aspect based on an annotation you just need to implement the Invoker interface shown above. Then you can create the dynamic proxy of the targeted object by calling:

Creating the Dynamic Proxy
1
2
3
// Some dao needed to be wrapped in Timeit aspect
SomeDao dao = ...
dao = Interceptor.createProxy(dao, Timeit.class, new TimerAspect());

Interceptor.createProxy takes 3 arguments: the targeted object to be proxied, the aspect annotation class and the object to handle the aspect. For the Timer (or Timeit) aspect, it could be as simple as this:

Timeit Aspect
1
2
3
4
5
6
7
8
9
10
11
public class TimerAspect implements Invoker<Timeit>
{
    @Override
  public Object execute(Method method, Object[] args, Timeit ann,
          RealInvoker realInvoker) throws Throwable {
      long start = System.nanoTime();
      Object result = realInvoker.invoke();
      System.out.println(String.format("-- %s tookkkk %s ms", method.getName(), (System.nanoTime() - start) / 1000000.0));
      return result;
  }
}

Here is why this is an aspect: the execute method takes note of the current time. It then invokes the original method call. Finally it calculates how long this method call takes. I believe in AspectJ this is called “before and around pointcut”.

Similarly, I would create the Retry aspect by implementing the Invoker interface and call

Creating Retry aspect
1
dao = Interceptor.createProxy(dao, Retry.class, new RetryAspect());

After we have the aspects to handle those annotations, how do chain them in a correct order, an order which makes sense at all? It all depends on your aspects’ logic but in this case I would make the Timer aspect outside of the Retry aspect. Confused? Here is the order of execution:

1.  Enter the Timer aspect, take note of the current time
2.  Enter the Retry aspect, retry count set to 0
3.  Invoke the actual Dao method
4.  If it fails, retry aspect catch the exception and retries! It keeps track of the number of retries (up to 3 times by default)
5.  Either the call fails if retries exceed 3 times or it exits the Retry aspect and yield the command to Timer Aspect again
6.  Timer aspect calculate how long this Dao method takes
7.  Return the result to the caller

One thing you need to pay close attention is the order of the execution of those chained aspects influenced by the way you create them. The inner most aspect will need to be created last. The outer most aspect will need to be created first. For this example, this is the order of aspect creation:

Order of Aspect creation
1
2
3
SomeDao dao = ...
dao = Interceptor.createProxy(dao, Retry.class, new RetryAspect());
dao = Interceptor.createProxy(dao, Timeit.class, new TimerAspect());

Here is the complete code in 2 simple classes. I hope you find this useful.

AbstractInvocationHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.lang.reflect.*;

public abstract class AbstractInvocationHandler<T> implements InvocationHandler
{

    protected T nextTarget;
    protected T realTarget = null;

    public AbstractInvocationHandler(T target)
    {
        nextTarget = target;
        if (nextTarget != null)
        {
            realTarget = findRealTarget(nextTarget);
            if (realTarget == null)
                throw new RuntimeException("findRealTarget failure");
        }
    }

    protected final T getRealTarget()
    {
        return realTarget;
    }

    protected static final <T> T findRealTarget(T t)
    {
        if (!Proxy.isProxyClass(t.getClass()))
            return t;
        InvocationHandler ih = Proxy.getInvocationHandler(t);
        if (AbstractInvocationHandler.class.isInstance(ih))
        {
            return (T) ((AbstractInvocationHandler) ih).getRealTarget();
        } else
        {
            try
            {
                Field f = findField(ih.getClass(), "target");
                if (Object.class.isAssignableFrom(f.getType())
                        && !f.getType().isArray())
                {

                    f.setAccessible(true); // suppress access checks
                    Object innerTarget = f.get(ih);
                    return (T) findRealTarget(innerTarget);
                }
                return null;
            } catch (NoSuchFieldException e)
            {
                return null;
            } catch (SecurityException e)
            {
                return null;
            } catch (IllegalAccessException e)
            {
                return null;
            } // IllegalArgumentException cannot be raised
        }
    }

    public static Field findField(Class cls, String name)
            throws NoSuchFieldException
    {
        if (cls != null)
        {
            try
            {
                return cls.getDeclaredField(name);
            } catch (NoSuchFieldException e)
            {
                return findField(cls.getSuperclass(), name);
            }
        } else
        {
            throw new NoSuchFieldException();
        }
    }
}
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;

interface RealInvoker
{
    public Object invoke() throws Throwable;
}

interface Invoker<A extends Annotation>
{
  public Object execute(final Method method, final Object[] args, A ann, RealInvoker realInvoker) throws Throwable;
}

class Interceptor<T, A extends Annotation> extends AbstractInvocationHandler<T>
{
  private Class<A> annotationClass;
  private Invoker<A> invoker;
  
  private Interceptor(T target, Class<A> annotationClass, Invoker<A> invoker)
    {
        super(target);
        this.annotationClass = annotationClass;
        this.invoker = invoker;
    }
  
  public static <T, A extends Annotation> T createProxy(T obj, Class<A> annotationClass, Invoker<A> invoker)
    {
        return (T) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                .getClass().getInterfaces(), new Interceptor(obj, annotationClass, invoker));
    }
  
  @Override
  public Object invoke(Object proxy, final Method method, final Object[] args)
          throws Throwable {
      Object result = null;
      A annotation = (A) this.realTarget.getClass().getMethod(method.getName()).getAnnotation(annotationClass);
        if (annotation != null)
        {
          return invoker.execute(method, args, annotation, new RealInvoker() {
              @Override
              public Object invoke() throws Throwable {
                  return method.invoke(Interceptor.this.nextTarget, args);
              }
          });
        }
        else
        {
            result = method.invoke(this.nextTarget, args);
        }

        return result;
  }
  
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Timeit
{
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Cache
{
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Retry
{
  int times() default 3;
}

interface Say
{
    public void say();
}

class ProxyFactory<T>
{
  public static <T> T createProxy(T obj)
  {
      obj = Interceptor.createProxy(obj, Retry.class, new Invoker<Retry>()
      {
          @Override
          public Object execute(Method method, Object[] args, Retry ann,
                  RealInvoker realInvoker) throws Throwable {
              Object result = null;
              
              int retries = 0;
              while (retries < ann.times())
              {
                  try
                  {
                      result = realInvoker.invoke();
                      break;
                  }
                  catch (Throwable crap)
                  {
                      retries++;
                      System.out.println(String.format("Crap catched %s times", retries));
                  }                    
              }
              
              if (retries >= ann.times()) throw new RuntimeException("Can't handle it anymore");
              
              return result;
          }
      });
      
      obj = Interceptor.createProxy(obj, Timeit.class, new Invoker<Timeit>()
      {
          @Override
          public Object execute(Method method, Object[] args, Timeit ann,
                  RealInvoker realInvoker) throws Throwable {
              System.out.println("-- Enter the timer");
              long start = System.nanoTime();
              Object result = realInvoker.invoke();
              System.out.println(String.format("-- %s tookkkk %s ms", method.getName(), (System.nanoTime() - start) / 1000000.0));
              return result;
          }
      });
      
      return obj;
  }
}

public class Main implements Say
{
    public static void main(String... args)
    {
        Say main = new Main();
        main = ProxyFactory.createProxy(main);

        for (int i = 0; i < 10; i++)
        {
          main.say();
          System.out.println(" ##### \n");
        }
    }

    @Timeit
    @Retry
    public void say()
    {
      Random rand = new Random();
      int r = rand.nextInt(9);
      if (r > 5)
      {
          throw new RuntimeException("CRAP rety!!");
      }
      
        System.out.println("Say hello");
    }
}
Cache Aspect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public CacheAspect implements Invoker
{
    ConcurrentMap<String, ConcurrentMap<Object, Object>> cache = new ConcurrentHashMap<>();
          
    @Override
    public Object execute(Method method, Object[] args, Cache ann,
          RealInvoker realInvoker) throws Throwable
    {
      System.out.println("@@ Entering cache proxy with " + method.getName() + " " + args);
        ConcurrentMap<Object, Object> theCache = cache.get(method.getName());
        if (theCache == null)
        {
          theCache = new ConcurrentHashMap<>();
          cache.put(method.getName(), theCache);
        }

        Object key = args;
        if (args == null)
        {
          key = "";
        }
        Object result = theCache.get(key);
        if (result != null)
        {
          System.out.println("@@ Cache hit with " + method.getName() + " " + args);
        }
        else
        {
          System.out.println("@@ Cache missed with " + method.getName() + " " + args);
          result = realInvoker.invoke();
          theCache.put(key, result == null ? "" : result);
        }

        System.out.println("@@ Exiting cache proxy");

      return result;
    }
}

Comments