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); } }
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
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