atom feed2 messages in com.googlegroups.google-web-toolkitDynamic proxy for asynchronous RPC in...
FromSent OnAttachments
Nathan WilliamsJul 21, 2007 2:07 am 
Nathan WilliamsJul 23, 2007 2:57 pm 
Subject:Dynamic proxy for asynchronous RPC interface
From:Nathan Williams (nlwi@gmail.com)
Date:Jul 21, 2007 2:07:17 am
List:com.googlegroups.google-web-toolkit

I'm writing my first enterprise GWT app, and I'm finding that despite the simplicity of the RPC mechanism there's still a lot to do on both sides of a remote method call. On the server side, there's user session authentication, role authorization, transaction management, and exception handling. On the client side, there's state and UI management, caching, and error handling. Since much of this code is boilerplate, the challenge has been finding ways to isolate it from the business logic in the service implementations on the server and the event logic on the client.

On the server side, I started with a servlet filter, but quickly realized that without ready access to the method signature or Java- level control over the return, that approach would be limited. Eventually, I came across what was done with the Spring GWTHandler (http://g.georgovassilis.googlepages.com/usingthegwthandler), and following that pattern was able to override RemoteServiceServlet#processCall with an implementation that dispatches methods through an InvocationHandler.

On the client, it's necessary to use different techniques. One workable approach is to just push redundant code into a base AsyncCallback implementation and subclass it for the needs of specific calls. What I've really found myself itching to be able to do, however, is wrap the asynchronous service interface in a dynamic proxy. This would allow me to associate generic behavior with service calls without having to remember to do anything special in my AsyncCallback at the point of call.

Which brings me to the reason for this post. Searches on the topic of dynamic proxy classes led me to an interesting and enlightening discussion about Generators (http://groups.google.com/group/Google-Web- Toolkit/msg/5c27255ce6949981), and I've applied that insight to implement a proxy Generator for asynchronous RPC interfaces. In the scope of what I've seen hints of others doing with generators for alternate serialization mechanisms and the like, this is relatively trivial, but I would like to share what I've learned. (If nothing else, perhaps this will illustrate some scattered concepts.)

------------------------------------------------------------

For starters, let's assume we have a service interface, its asynchronous interface, and the standard setup code:

------------------------------------------------------------ public interface FooService extends RemoteService { public String foo(String s); }

public interface FooServiceAsync { public void foo(String s, AsyncCallback callback); }

public static final FooServiceAsync FOO_SERVICE = (FooServiceAsync) GWT.create(FooService.class); static { ((ServiceDefTarget) FOO_SERVICE).setServiceEntryPoint(GWT.getModuleBaseURL() + "fooService"); }

------------------------------------------------------------

The goal is to wrap FOO_SERVICE in a proxy class that will allow generic control over service method invocation.

Similar to the conventional Java proxy class pattern, we'll need an InvocationHandler interface, but since we can't invoke the method using reflection, our "handler" is going to be more of a lifecycle method:

------------------------------------------------------------ /** * Used to inject code before and following (hence extending from AsyncCallback) the proxied invocation of an asynchronous RPC method. */ public interface AsyncInvocationHandler extends AsyncCallback { /** * Called before method invocation. * @return true to invoke the method; false to skip it * @param methodName the name of the method * @param args the arguments (of the service version of the method; not the async version) * @param clientCallback the callback passed to the proxy by the service client when calling the async version of the method */ public boolean preInvoke(String methodName, Object[] args, AsyncCallback clientCallback); }

------------------------------------------------------------

Assuming we have a proxy instance, we'll need a way to configure it, so we'll need an interface (similar in concept to the role of the ServiceDefTarget interface) to enable that:

------------------------------------------------------------ public interface AsyncProxy { /** * Call to bind a proxy instance to an asynchronous service instance and an invocation handler. * @param serviceProxy an asynchronous service instance obtained from GWT.create() * @param serviceDefTarget a convenience parameter; pass to init the entry point of serviceProxy; null is ignored * @param handler the handler to use to control method invocations */ public void bind(Object serviceProxy, String entryPoint, AsyncInvocationHandler handler); }

------------------------------------------------------------

If we were to manually write a class to proxy FooServiceAsync, it might look like this:

------------------------------------------------------------ public class FooServiceAsyncProxy implements FooServiceAsync, AsyncProxy {

private FooServiceAsync fService; private AsyncInvocationHandler fHandler;

public void bind(Object serviceAsync, String entryPoint, AsyncInvocationHandler handler) { fService = (FooServiceAsync) serviceAsync; if (entryPoint != null) { ((ServiceDefTarget) fService).setServiceEntryPoint(entryPoint); } fHandler = handler; }

/** Repeat this method template for every method of FooServiceAsync */ public void foo(java.lang.String s, final com.google.gwt.user.client.rpc.AsyncCallback callback) { if (fHandler.preInvoke("foo", new Object[] { s }, callback)) { fService.foo(s, new AsyncCallback() { public void onSuccess(Object result) { fHandler.onSuccess(result); // our injected handler callback.onSuccess(result); // the client's handler }

public void onFailure(Throwable t) { fHandler.onFailure(t); callback.onFailure(t); } }); } } }

------------------------------------------------------------

This is clearly a job for a code generator. ...Which is exactly what GWT's Generator mechanism is. The following isn't pretty, but it will generate the above class for any service interface:

------------------------------------------------------------ public class AsyncProxyGenerator extends Generator { public String generate(TreeLogger logger, GeneratorContext context, String asyncServiceTypeName) throws UnableToCompleteException { try { TypeOracle typeOracle = context.getTypeOracle();

JClassType requestedClass = typeOracle.getType(asyncServiceTypeName); if (requestedClass.isInterface() == null) { logger.log(TreeLogger.ERROR, "Class '" + asyncServiceTypeName + "' is not an interface", null); throw new UnableToCompleteException(); }

String packageName = requestedClass.getPackage().getName(); String qualifiedRequestedClassName = requestedClass.getQualifiedSourceName();

String proxyClassName = requestedClass.getSimpleSourceName() + "Proxy"; String qualifiedProxyClassName = packageName + "." + proxyClassName;

PrintWriter printWriter = context.tryCreate(logger, packageName, proxyClassName); if (printWriter != null) { ClassSourceFileComposerFactory cf = new ClassSourceFileComposerFactory(packageName, proxyClassName);

cf.addImport("com.google.gwt.user.client.rpc.AsyncCallback");

cf.addImport("com.google.gwt.user.client.rpc.ServiceDefTarget");

cf.addImport("mypackage.mymodule.client.proxy.AsyncInvocationHandler");

cf.addImport("mypackage.mymodule.client.proxy.AsyncProxy"); cf.addImport(qualifiedRequestedClassName); cf.addImplementedInterface(requestedClass.getName()); cf.addImplementedInterface("AsyncProxy");

SourceWriter sourceWriter = cf.createSourceWriter(context, printWriter);

if (sourceWriter != null) { sourceWriter.println(); sourceWriter.println("private " + requestedClass.getName() + " fService;"); sourceWriter.println("private AsyncInvocationHandler fHandler;");

writeBindMethod(sourceWriter, requestedClass);

JMethod[] methods = requestedClass.getMethods(); for (int i = 0; i < methods.length; i++) { JMethod method = methods[i];

if ("bind".equals(method.getName())) { continue; } writeInterfaceMethod(logger, sourceWriter, method); } sourceWriter.println("}"); sourceWriter.commit(logger); } } return qualifiedProxyClassName; } catch (NotFoundException e) { logger.log(TreeLogger.ERROR, "Class '" + asyncServiceTypeName + "' Not Found", e); throw new UnableToCompleteException(); } }

protected void writeBindMethod(SourceWriter sourceWriter, JClassType requestedClass) { sourceWriter.println(); sourceWriter.println("public void bind(Object serviceAsync, String entryPoint, AsyncInvocationHandler handler) {"); sourceWriter.indent(); sourceWriter.print("fService = ("); sourceWriter.print(requestedClass.getName()); sourceWriter.println(") serviceAsync;"); sourceWriter.println("if (entryPoint != null) {"); sourceWriter.indent(); sourceWriter.println("((ServiceDefTarget) fService).setServiceEntryPoint(entryPoint);"); sourceWriter.outdent(); sourceWriter.println("}"); sourceWriter.println("fHandler = handler;"); sourceWriter.outdent(); sourceWriter.println("}"); }

protected void writeInterfaceMethod(TreeLogger logger, SourceWriter sourceWriter, JMethod m) throws UnableToCompleteException { sourceWriter.println();

sourceWriter.print("public ");

sourceWriter.print(m.getReturnType().getQualifiedSourceName()); sourceWriter.print(" "); sourceWriter.print(m.getName()); sourceWriter.print("("); int clientCallback = -1; JParameter[] params = m.getParameters(); for (int i = 0; i < params.length; i++) { if (i > 0) { sourceWriter.print(", "); } if (paramImplements(params[i].getType(), AsyncCallback.class)) { clientCallback = i; sourceWriter.print("final "); }

sourceWriter.print(params[i].getType().getQualifiedSourceName()); sourceWriter.print(" "); sourceWriter.print(params[i].getName()); } if (clientCallback == -1) { logger.log(logger.ERROR, "Method " + m.getName() + " does not have an AsyncCallback parameter.", null); throw new UnableToCompleteException(); }

sourceWriter.print(""); sourceWriter.print(")"); JType[] thrown = m.getThrows(); for (int i = 0; i < thrown.length; i++) { if (i > 0) { sourceWriter.print(", "); } sourceWriter.print(thrown[i].getQualifiedSourceName()); } sourceWriter.println(" {");

sourceWriter.indent();

sourceWriter.print("if (fHandler.preInvoke(\""); sourceWriter.print(m.getName()); sourceWriter.print("\", new Object[] { "); boolean first = true; for (int i = 0; i < params.length; i++) { if (i != clientCallback) { if (first) { first = false; } else { sourceWriter.print(", "); } sourceWriter.print(params[i].getName()); } } sourceWriter.print(" }, "); sourceWriter.print(params[clientCallback].getName()); sourceWriter.println(")) {"); sourceWriter.indent(); sourceWriter.print("fService."); sourceWriter.print(m.getName()); sourceWriter.print("("); for (int i = 0; i < params.length; i++) { if (i > 0) { sourceWriter.print(", "); } if (i != clientCallback) { sourceWriter.print(params[i].getName()); } else { sourceWriter.println("new AsyncCallback() {"); sourceWriter.indent(); sourceWriter.println("public void onSuccess(Object result) {"); sourceWriter.indent(); sourceWriter.println("fHandler.onSuccess(result);"); sourceWriter.print(params[i].getName()); sourceWriter.println(".onSuccess(result);"); sourceWriter.outdent(); sourceWriter.println("}"); sourceWriter.println(); sourceWriter.println("public void onFailure(Throwable t) {"); sourceWriter.indent(); sourceWriter.println("fHandler.onFailure(t);"); sourceWriter.print(params[i].getName()); sourceWriter.println(".onFailure(t);"); sourceWriter.outdent(); sourceWriter.println("}"); sourceWriter.outdent(); sourceWriter.print("}"); } }

sourceWriter.println(");"); sourceWriter.outdent(); sourceWriter.println("}"); }

protected boolean paramImplements(JType type, Class c) { JClassType classType = type.isClassOrInterface(); if (classType != null) { if (classType.getQualifiedSourceName().equals(c.getName())) { return true; } JClassType[] interfaces = classType.getImplementedInterfaces(); for (int i = 0; i < interfaces.length; i++) { if (interfaces[i].getQualifiedSourceName().equals(c.getName())) { return true; } } } return false; } }

------------------------------------------------------------

Generators are invoked automatically as part of the GWT compile process, but they have to be explicitly bound to the classes they affect in the gwt.xml:

------------------------------------------------------------ <!-- When I ask for GWT.create(FooServiceAsync.class), invoke this Generator and return an instance of the class it creates instead. --> <generate-with class="mypackage.mymodule.rebind.AsyncProxyGenerator"> <when-type-assignable class="mypackage.mymodule.client.service.FooServiceAsync" /> </generate-with>

------------------------------------------------------------

So, now we have all the pieces necessary to wrap method calls to our asynchronous service interface. Here's the original and revised setup code:

------------------------------------------------------------ // before public static final FooServiceAsync FOO_SERVICE = (FooServiceAsync) GWT.create(FooService.class); // FOO_SERVICE is an RPC proxy for FooService (magically converted to FooServiceAsync as a special case for RPC) static { ((ServiceDefTarget) FOO_SERVICE).setServiceEntryPoint(GWT.getModuleBaseURL() + "fooService"); }

// after public static final FooServiceAsync FOO_SERVICE = (FooServiceAsync) GWT.create(FooServiceAsync.class); // FOO_SERVICE is a proxy for FooServiceAsync static { // Our generated proxy implements AsyncProxy allowing us to cast and call bind. ((AsyncProxy) FOO_SERVICE).bind( // target of async proxy is the same RPC proxy for FooService we were using before GWT.create(FooService.class),

// pass the entry point URL while we're at it GWT.getModuleBaseURL() + "fooService",

// And now, we can wrap every service method call with these lifecycle methods new AsyncInvocationHandler() { public boolean preInvoke(String methodName, Object[] args, AsyncCallback clientCallback) { return true; }

public void onSuccess(Object result) { }

public void onFailure(Throwable caught) { } } ); }

------------------------------------------------------------

Some examples of how an AsyncInvocationHandler could be used with the proxy: - Perform some common check before every call - Reject calls that are invalid for the application state (not the only place to do this, but another option) - Skip the remote call and return a cached value to the client's AsyncCallback (recommend using a DeferredCommand so client has a chance to finish event loop before receiving callback) - Set/restore state of application around the call. (loading page, etc.) - Handle errors in a consistent way across the application

Things you have to do differently from the normal RPC definition/setup/ call pattern to use a proxy: - Add the generate-with element to your gwt.xml. - Wrap the RPC proxy with the custom proxy and bind when setting up the service instance.

Finally, it should be noted that the interplay between the AsyncInvocationHandler and the proxy implementation need not follow the exact pattern illustrated here. The lifecycle methods and style of invocation and flow control can be changed by revising the handler interface and updating the generator code to drive the desired behavior.