Wednesday, October 28, 2015

Annotation processing during runtime

This post demonstrates how to process the annotation during runtime in order to add on additional functionalities to the target object without changing the object implementation.

The existing Account implementation:

Given the Account class below:

public interface IAccount {
    void withdraw();
    void deposit();
    void longProcess();
}

public class Account implements IAccount {
    
    public void withdraw() {
        System.out.println("Dummy method for withdraw..");
    }
    
    public void deposit() {
        System.out.println("Dummy method for deposit..");
    }
    
    public void longProcess() {
        System.out.println("Dummy method for longProcess..");
        try {
            Random random = new Random();
            int x = random.nextInt(4);
            Thread.sleep(x * 1000);
        } catch (InterruptedException ex) {
        }
    }
}

The method implementations are dummies. They are just pretending to carry on some specific accounting operations which we don't really care in this example. The more important part is how can we add additional functionality without changing the existing method implementation.

Requirement:

Capture the following information while an Account object is being accessed during runtime.
  • The invocation frequency of an accounting operation. By default, this data capturing is ON.
  • The longest processing time spent of an accounting operation. By default, this data capturing is OFF.

Solution:

Make use of Java Annotation and Java Dynamic Proxy.

Annotation interface:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Capture {
    public boolean invokeCount() default true;
    public boolean longestTimeSpent() default false;
}

First of all, we create an annotation interface called Capture. It has declared 2 annotation attributes to meet the requirement respectively. This annotation is only applied to Java methods. It's retention policy has to be RUNTIME so that, the annotated method is able to access the annotation information during runtime.

Apply annotation:

public class Account implements IAccount {
    
    @Capture
    public void withdraw() {
        System.out.println("Dummy method for withdraw..");
    }
    
    @Capture
    public void deposit() {
        System.out.println("Dummy method for deposit..");
    }
    
    @Capture(longestTimeSpent = true)
    public void longProcess() {
        System.out.println("Dummy method for longProcess..");
        try {
            Random random = new Random();
            int x = random.nextInt(4);
            Thread.sleep(x * 1000);
        } catch (InterruptedException ex) {
        }
    }
}

Account methods now are annotated with Capture annotation accordingly.

Invocation Handler:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CaptureInvocationHandler implements InvocationHandler {

    private final IAccount realAccount; // The real Account object
    private final Map<String, Integer> methodVisitFrequency;
    private final Map<String, Long> methodLongestTimeSpent;
    
    public CaptureInvocationHandler(IAccount account) {
        this.realAccount = account;
        this.methodVisitFrequency = new HashMap<>();
        this.methodLongestTimeSpent = new HashMap<>();
    }

    public Map<String, Integer> getMethodVisitFrequency() {
        return methodVisitFrequency;
    }

    public Map<String, Long> getMethodLongestTimeSpent() {
        return methodLongestTimeSpent;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method m = realAccount.getClass()
                .getMethod(method.getName(), method.getParameterTypes());
        if (m.isAnnotationPresent(Capture.class)) {
            String methodName = m.getName();
            
            // If invokeCount is ON
            if (m.getAnnotation(Capture.class).invokeCount()) {
                // Capture invocation frequency of the method.
                Integer frequency = methodVisitFrequency.get(methodName);
                if (frequency == null) {
                    methodVisitFrequency.put(methodName, 1);
                } else {
                    methodVisitFrequency.put(methodName, frequency + 1);
                }
            } 
            
            // If longestTimeSpent is ON
            if (m.getAnnotation(Capture.class).longestTimeSpent()) { 
                // Capture longest processing time spent of the method.
                long startTime = System.currentTimeMillis();
                Object returnObj = m.invoke(realAccount, args);
                long endTime = System.currentTimeMillis();
                long timeSpent = endTime - startTime;
                
                Long lastTimeSpent = methodLongestTimeSpent.get(methodName);
                if (lastTimeSpent == null) {
                    methodLongestTimeSpent.put(methodName, timeSpent);
                } else if (timeSpent > lastTimeSpent) {
                    methodLongestTimeSpent.put(methodName, timeSpent);
                }
                return returnObj;
            }
        }
        return m.invoke(realAccount, args);
    }
}

Invocation Handler is the bridge between the real object and the proxy object in Java Dynamic Proxy. CaptureInvocationHandler above is able to intercept the Account object method invocation and capture the desired data accordingly.

Application:

public static void main(String[] args) {

    // Invocation handler that keep the real Account object.
    CaptureInvocationHandler handler = new CaptureInvocationHandler(new Account());

    // Create a Account proxy object with CaptureInvocationHandler
    // as bridge to real Account object
    IAccount proxiedAcct
            = (IAccount) Proxy.newProxyInstance(Account.class.getClassLoader(),
                    new Class[]{IAccount.class}, handler);

    /*** Invoke Account operations - start ***/
    proxiedAcct.deposit();
    proxiedAcct.deposit();
    proxiedAcct.deposit();

    proxiedAcct.withdraw();
    proxiedAcct.withdraw();

    proxiedAcct.longProcess();
    proxiedAcct.longProcess();
    /*** Invoke Account operations - end ***/

    /*** Print captured data - start ***/
    for (Map.Entry<String, Integer> entry
            : handler.getMethodVisitFrequency().entrySet()) {
        System.out.println(entry.getKey() + " " + entry.getValue());
    }

    for (Map.Entry<String, Long> entry
            : handler.getMethodLongestTimeSpent().entrySet()) {
        System.out.println(entry.getKey() + " " + entry.getValue() + "ms");
    }
    /*** Print captured data - end ***/
}

Result:
Dummy method for deposit..
Dummy method for deposit..
Dummy method for deposit..
Dummy method for withdraw..
Dummy method for withdraw..
Dummy method for longProcess..
Dummy method for longProcess..
deposit 3
longProcess 2
withdraw 2
longProcess 2006ms

Summary:

As you can see, besides adding the annotation onto methods in Account class, there is no change in method implementation. The method access information capturing is done in Invocation Handler. If the Account proxy object creation is bothering you, then you actually could move the object creation into a Factory Method. If you still don't like the way Java Dynamic Proxy works, check out the CGLib in my other post - What makes the CGLib Proxy Enhancer the good substitution for the Java Dynamic Proxy.



No comments: