Using Java's Proxy Class
Java 1.3 introduced some handy new classes java.lang.reflect.Proxy and the java.lang.reflect.InvocationHandler; basically this allows you to dynamically create a proxy class that implements given set of interfaces and wrap this proxy class around a concret implementation of the class. It works by implementing the InvocationHandler interface which is called by the proxy prior to calling the wrapped class. Your implementation of the InvocationHandler can then choose to invoke the wrapped class, throw an exception, call another class, or anything else you may want it to do. At first it does not seem apparent why this is needed but they real power comes in building generic frameworks such as Hibernate and EJB3 application servers where offen times you are working with proxies instead of concreate implementations.
Recently I was asked to create a functional test to call a set of three web services for every member in our system. This was driven by the fact that we are integrating to a new system and there is a large data migration effort that is currently underway; the steak holders found it to be crucial that every member was migrated properly. The first thing that came to mind was how to deal with the large dataset, just under two million members, but the problems with dealing with datasets of this size have mostly been solved. So with no real problems facing me I was simple going to build a few very large loops and let the test run; but then I started to think about reusability.
Today we are calling three methods for all members, what about running one method on all accounts, or we can run all the plan methods against all our promocodes that have a free trial period. Since these are all plausible tests in not only production but within dev as well I wanted to make this as reusable and helpful as possible. Currently we have all our functional test calling web services clients that were generated by using JAX-WS. We have a ServiceFactory class that is used to create all web service clients for our functional test. So if you can wrap a proxy around these web services you can then monitor and record any method call you like in detail.
To start out I first I created a Recorder interface that would provide the contract on how record a service call.
public interface Recorder {
public void record(Method method, Object[] params, Object returned);
public void record(Method method, Object[] params, Throwable thrown);
}
Since we are only wanting to track a few service calls and not all the methods the service provides I created an abstract class for connivence.
abstract public class AbstractRecorder implements Recorder {
List<Method> recordingMethods = new ArrayList<Method>();
public void add(Method method) {
recordingMethods.add(method);
}
public void add(Class clazz, String method, Class... params)
throws SecurityException, NoSuchMethodException {
add(clazz.getMethod(method, params));
}
public void add(Class clazz) {
for (Method method : clazz.getMethods()) {
recordingMethods.add(method);
}
}
public boolean isRecording(Method method) {
return recordingMethods.contains(method);
}
}
Now that we have a contract for recording methods we can now implement the InvocationHandler interface and fulfill the contact for the proxy.
public class ServiceInvocationHandler<I> implements InvocationHandler {
private final Recorder recorder;
private final I service;
private ServiceInvocationHandler(I service, Recorder recorder) {
this.service = service;
this.recorder = recorder;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returned;
try {
returned = method.invoke(service, args);
} catch (Throwable t) {
recorder.record(method, args, t);
throw t;
}
recorder.record(method, args, returned);
return returned;
}
public static <I> I createInvocationHandler(I service, Recorder recorder) {
InvocationHandler hanlder = new ServiceInvocationHandler<I>(service, recorder);
return (I) Proxy.newProxyInstance(service.getClass().getClassLoader(),
new Class[] { service.getClass() }, hanlder);
}
}
The object stores an instance of the concert class that is being wrapped and an instance of the recorder that will be used to record the service call. We will use the createInvocationHandler method to create proxy that wraps the service and also to create the InvocationHandler. When creating the proxy using newProxyInstance the first parameter is the classloader for the proxy. The second parameter is and array of interfaces you want the proxy to intercept, this means you can only intercept and wrap public methods since interfaces can only have public methods. The third paramater is the concreate class you would like to wrap.
For the most part I am ready to use my proxy. I can simply create a new service using my ServiceFactory and call createInvocationHandler which will wrap the service. But I wanted to place another layer of abstraction on creating the recordable service so any developer that needs to record service calls need to simple create a class that implements Recorder.
public class ServiceRecorderFactory {
private final Recorder recorder;
public ServiceRecorderFactory(Recorder recorder) {
this.recorder = recorder;
}
public <I> I get(I serviceInterface) {
I service = ServiceFactory.get(serviceInterface);
return ServiceInvocationHandler.createInvocationHandler(service, recorder);
}
}
When creating a ServiceRecorderFactory they would simply provide the Recorder you implemented and then they can request services just like before. It will simply call the normal ServiceFactory to create the service, create a proxy that wraps the service, then returns the proxy. Here is an example of it in use.
public class TestMember {
public static void main(String[] args) {
JdbcRecorder recorder = new JdbcRecorder();
recorder.add(MemberService.class, "getMember", Long.class);
ServiceRecorderFactory serviceFactory = new ServiceRecorderFactory(recorder);
MemberService memberService = serviceFactory.get(MemberService.class);
Long[] members = new Long[]{1L, 2L, 4L, 5L, 6L};
for(Long memberId : members){
memberService.getMember(memberId); //will record
memberService.getAddress(memberId); //will not record
}
}
}