Being a long-time, hardcore Spring user, I was unpleasantly surprised to see that GWT-RPC's server-side model requires subclassing the supplied RemoteServiceServlet and mapping this in web.xml... ugh. Thankfully, the flexibility of Spring led to a solution that allows GWT-RPC services to be instantiated as with any other bean.
The solution has two facets - the infrastructure components that are wired up once, and the service components that you write as requirements demand. The emphasis was on simplifying the service components such that the RPC plumbing didn't get in the way of business logic.
The result:
package digitalascent.web.support.gwt;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.stereotype.Controller;
@Controller
@GwtRpcEndPoint
public class ServerServiceImpl extends ApplicationObjectSupport implements ServerStatusService {
public String getStatusData() {
return "Your random number: " + Math.random();
}
}
...nice and simple! The differences from a conventional GWT-RPC service:
- No requirement to inherit from RemoteServiceServlet (or any other class for that matter);
- Addition of the @GwtRpcEndPoint annotation to indicate that this bean is an end point for GWT-RPC;
- Automagic mapping of URLs (more on this below);
- Ability to use all of Spring's DI, AOP and other services on the bean (its no different than any other bean);
- I've chosen to use @Controller so that Spring 2.5's component scanning finds and registers this bean; alternately, @Controller could be omitted and the bean simply registered via XML.
- @GwtRpcEndPoint; this annotation is applied to the service implementation class to indicate that it is the end point for GWT-RPC calls. Alternately, the selection logic could have simply looked for the RemoteService interface, however, that would prevent exclusion of some RemoteServices when necessary;
- GwtAnnotationHandlerMapping; this is a Spring HandlerMapping that introspects all registered beans looking for ones annotated with @GwtRpcEndPoint. When a GWT-RPC end point is found, its url is registered (more on URL mapping below);
- GwtRpcEndPointHandlerAdapter; this is a Spring HandlerAdapter that performs all the GWT-RPC goodies - decoding the request, invoking the method on the service bean, and encoding the response.
This of course requires that the GWT client side code match up - a simple convenience method helps with this:
public static void setRpcEndPointUrl(Object proxy) {
ServiceDefTarget target = (ServiceDefTarget) proxy;
StringBuffer sb = new StringBuffer( );
sb.append( GWT.getTypeName(proxy) );
String endPointName = sb.substring( 0, sb.indexOf("_Proxy") );
endPointName = endPointName.replace('.','/');
target.setServiceEntryPoint("/gwt-rpc/" + endPointName + ".do");
}
...and, for those special cases, you can specify a specific URL in the @GwtRpcEndPoint annotation.
Here's the rest of the code:
GwtRpcEndPoint.java
package digitalascent.web.support.gwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface GwtRpcEndPoint {
String value() default "";
}
GwtAnnotationHandlerMapping .java
package digitalascent.web.support.gwt;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping;
import com.google.gwt.user.client.rpc.RemoteService;
/**
* Spring HandlerMapping that detects beans annotated with @GwtRpcEndPoint and registers their
* URLs.
*
* @author Chris Lee
*
*/
public class GwtAnnotationHandlerMapping extends AbstractDetectingUrlHandlerMapping {
private String prefix = "";
private String suffix = "";
protected String[] buildUrls(Class handlerType, String beanName) {
String remoteServiceName = null;
Class[] interfaces = handlerType.getInterfaces();
for (Class itf : interfaces) {
// find the interface that extends RemoteService
if (RemoteService.class.isAssignableFrom(itf)) {
remoteServiceName = itf.getName();
}
}
if (remoteServiceName == null) {
throw new IllegalArgumentException("Unable to generate name for " + handlerType.getName()
+ "; cannot locate interface that is a subclass of RemoteService");
}
String classPath = StringUtils.replace(remoteServiceName, ".", "/");
StringBuilder sb = new StringBuilder();
sb.append(prefix);
sb.append(classPath);
sb.append(suffix);
return new String[] { sb.toString() };
}
@Override
protected final String[] determineUrlsForHandler(String beanName) {
String[] urls = new String[0];
Class handlerType = getApplicationContext().getType(beanName);
if (handlerType.isAnnotationPresent(GwtRpcEndPoint.class)) {
GwtRpcEndPoint endPointAnnotation = handlerType.getAnnotation(GwtRpcEndPoint.class);
if (StringUtils.hasText(endPointAnnotation.value())) {
urls = new String[] { endPointAnnotation.value() };
} else {
urls = buildUrls(handlerType, beanName);
}
}
return urls;
}
public final void setPrefix(String prefix) {
this.prefix = prefix;
}
public final void setSuffix(String suffix) {
this.suffix = suffix;
}
}
GwtRcpEndPointHandlerAdapter.java
package digitalascent.web.support.gwt;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
/**
* Spring HandlerAdapter to dispatch GWT-RPC requests. Relies on handlers registered by GwtAnnotationHandlerMapper
*
* @author Chris Lee
*
*/
public class GwtRcpEndPointHandlerAdapter extends RemoteServiceServlet implements HandlerAdapter {
private static ThreadLocal