This is the second part of my series on creating your own AOP solution. The first can be found here.
The last post left off having proxied our interfaces so that method calls could be intercepted; pre and post-processing could be performed. Some examples of this processing would be logging, security, caching, etc. At this point it is fairly obvious that the current solution has a significant drawback; there is only one chance to intercept the call. Ideally, the invocation handlers could be chained dynamically. However, the current solution requires an implementation for each possible chain. This is unacceptable.
To resolve this issue I defined two “interceptors”, a pre and post method interceptor. Here is the interface definition:
public interface PreMethodInterceptor {
void preMethod(Object proxy, Method method, Object[] params, Object target, InterceptorAdvice advice);
}
public interface PostMethodInterceptor {
void postMethod(Object proxy, Method method, Object[] params, Object target, InterceptorAdvice advice);
}
Now an invocation handler can be populated with any combination of interceptors, and the code will look like this:
public abstract class ProxyInvocationHandler implements InvocationHandler {
private static final Logger log = Logger.getLogger(ProxyInvocationHandler.class);
private List<PreMethodInterceptor> preInterceptors;
private List<PostMethodInterceptor> postInterceptors;
private Object target;
public ProxyInvocationHandler(Object target, List<PreMethodInterceptor> pre, List<PostMethodInterceptor> post) {
this.target = target;
this.preInterceptors = pre;
this.postInterceptors = post;
}
public Object invoke(Object proxy, Method method, Object[] parameters) throws Throwable {
HandlerAdvice advice = new UniversalAdvice();
Object object = null;
for (PreMethodInterceptor pre : this.preInterceptors) {
if (advice.continueIntercepting()) {
pre.preMethod(proxy, method, parameters, this.target, advice.getInterceptorAdvice());
}
}
if (advice.invokeTargetMethod()) {
object = method.invoke(this.target, parameters);
advice.getInterceptorAdvice().addInterceptionResult(target, object);
} else {
object = advice.getInterceptorAdvice().getLastResult(target, method.getReturnType());
}
advice.reset();
for (PostMethodInterceptor post : this.postInterceptors) {
if (advice.continueIntercepting()) {
post.postMethod(proxy, method, parameters, this.target, advice.getInterceptorAdvice());
}
}
return object;
}
}
Now, each desired interceptor is processed before and after the method call.
The next obvious improvement will be how to handle exceptions, failures and other behavior altering logic. Take this case of an interceptor chain, inspired by my cache/performance task defined in part one.
Log -> Performance -> Cache -> Method -> Cache -> Performance
Obviously, if the cache holds the object graph we are interested in, we do not want to execute the method or perform the post-cache interception. In that case we would ideally want this:
Log -> Performance -> Cache -> Performance
Yet it is vital that each interceptor is ignorant of other interceptors. We cannot expect to share state between the interceptors (although that is completely possible, its an assumption that we should not make). So stay tuned for part three, where we tackle this problem.