Dynamic proxy for asynchronous RPC interface

46 views
Skip to first unread message

Nathan Williams

unread,
Jul 21, 2007, 5:07:17 AM7/21/07
to 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.

Nathan Williams

unread,
Jul 23, 2007, 5:57:36 PM7/23/07
to Google Web Toolkit
I found a couple of bugs in AsyncProxyGenerator which are fixed below.

------------------------------------------------------------
public class AsyncProxyGenerator extends Generator {
public String generate(TreeLogger logger, GeneratorContext context,

String typeName) throws UnableToCompleteException {


try {
TypeOracle typeOracle = context.getTypeOracle();

JClassType requestedClass = typeOracle.getType(typeName);
if (requestedClass.isInterface() == null) {
logger.log(TreeLogger.ERROR, "Class '" + typeName + "' 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 out = cf.createSourceWriter(context, printWriter);

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

writeBindMethod(out, 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, out, method);
}
out.commit(logger);


}
}
return qualifiedProxyClassName;
} catch (NotFoundException e) {

logger.log(TreeLogger.ERROR, "Class '" + typeName + "' Not Found",
e);
throw new UnableToCompleteException();
}
}

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

protected void writeInterfaceMethod(TreeLogger logger, SourceWriter

out, JMethod m) throws UnableToCompleteException {
out.println();

out.print("public ");
out.print(m.getReturnType().getQualifiedSourceName());
out.print(" ");
out.print(m.getName());
out.print("(");


int clientCallback = -1;
JParameter[] params = m.getParameters();
for (int i = 0; i < params.length; i++) {
if (i > 0) {

out.print(", ");


}
if (paramImplements(params[i].getType(), AsyncCallback.class)) {
clientCallback = i;

out.print("final ");
}
out.print(params[i].getType().getQualifiedSourceName());
out.print(" ");
out.print(params[i].getName());


}
if (clientCallback == -1) {
logger.log(logger.ERROR, "Method " + m.getName() + " does not have
an AsyncCallback parameter.", null);
throw new UnableToCompleteException();
}

out.print("");
out.print(")");


JType[] thrown = m.getThrows();
for (int i = 0; i < thrown.length; i++) {
if (i > 0) {

out.print(", ");
}
out.print(thrown[i].getQualifiedSourceName());
}
out.println(" {");

out.indent();

out.print("if (fHandler.preInvoke(\"");
out.print(m.getName());
out.print("\", new Object[] { ");


boolean first = true;
for (int i = 0; i < params.length; i++) {
if (i != clientCallback) {
if (first) {
first = false;
} else {

out.print(", ");
}
JPrimitiveType primitive = params[i].getType().isPrimitive();
if (primitive != null) {
out.print("new ");
if (primitive.equals(JPrimitiveType.INT)) {
out.print("Integer");
} else if (primitive.equals(JPrimitiveType.CHAR)) {
out.print("Character");
} else {
out.print(primitive.getSimpleSourceName().substring(0,
1).toUpperCase());
out.print(primitive.getSimpleSourceName().substring(1));
}
out.print("(");
out.print(params[i].getName());
out.print(")");
} else {
out.print(params[i].getName());
}
}
}
out.print(" }, ");
out.print(params[clientCallback].getName());
out.println(")) {");
out.indent();
out.print("fService.");
out.print(m.getName());
out.print("(");


for (int i = 0; i < params.length; i++) {
if (i > 0) {

out.print(", ");
}
if (i != clientCallback) {
out.print(params[i].getName());
} else {
out.println("new AsyncCallback() {");
out.indent();
out.println("public void onSuccess(Object result) {");
out.indent();
out.println("fHandler.onSuccess(result);");
out.print(params[i].getName());
out.println(".onSuccess(result);");
out.outdent();
out.println("}");
out.println();
out.println("public void onFailure(Throwable t) {");
out.indent();
out.println("fHandler.onFailure(t);");
out.print(params[i].getName());
out.println(".onFailure(t);");
out.outdent();
out.println("}");
out.outdent();
out.print("}");
}
}

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

Reply all
Reply to author
Forward
0 new messages