Monday, November 12, 2007

GWT-RPC with Spring 2.x

Had some time recently to play with the Google Web Toolkit (GWT). One of my goals was to better understand its AJAX RPC model.

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.
So where does the magic happen?
  1. @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;
  2. 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);
  3. 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.
How are GWT-RPC urls built? I'm a *huge* fan of convention-over-configuration - it enforces consistency and scales well. By default, GWT-RPC urls are mapped by taking the fully-qualified class name of the interface that extends the RemoteService interface - in this case, digitalascent.web.support.gwt.ServiceStatusService and turning it into a URL by prepending a prefix, converting '.'s to '/'s and appending a suffix (the prefix and suffix are configurable properties of the bean). So we get /gwt-rpc/digitalascent/web/support/gwt/ServiceStatusService.do (where the prefix is '/gwt-rpc/' and the suffix is '.do'). A nice side effect - this is impervious to refactoring, as both the client and server URLs are derived from the same single fully-qualifed interface name.

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 handlerHolder = new ThreadLocal();

private static final long serialVersionUID = -7421136737990135393L;

public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}

public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

try {
// store the handler for retrieval in processCall()
handlerHolder.set(handler);
doPost(request, response);
} finally {
// clear out thread local to avoid resource leak
handlerHolder.set(null);
}

return null;
}

protected Object getCurrentHandler() {
return handlerHolder.get();
}

public boolean supports(Object handler) {
return handler instanceof RemoteService && handler.getClass().isAnnotationPresent(GwtRpcEndPoint.class);
}

@Override
public String processCall(String payload) throws SerializationException {
/*
* The code below is borrowed from RemoteServiceServet.processCall, with the following changes:
*
* 1) Changed object for decoding and invocation to be the handler (versus the original 'this')
*/

try {
RPCRequest rpcRequest = RPC.decodeRequest(payload, getCurrentHandler().getClass());

String retVal = RPC.invokeAndEncodeResponse(getCurrentHandler(), rpcRequest.getMethod(), rpcRequest
.getParameters());

return retVal;

} catch (Throwable t) {
return RPC.encodeResponseForFailure(null, t);
}
}
}

Wednesday, May 23, 2007

Using Spring Framework to enable multi-environment deployment without rebuilding or repackaging.

Each software development project must inevitably deploy its artifacts at multiple points in its lifecycle. Often, these artifacts need to be deployed to multiple environments, such as development, integration, staging / qual, production (the number, naming & uses of these environments is beyond the scope of this article). Different environments will have different configurations; for example, in development there may be additional instrumentation for testing purposes that wouldn't be appropriate for a production environment. Or perhaps there are differing resource paths or other configuration values between environments.

A common pattern for accomplishing this has been a 'search and replace' during the build process (hopefully automated using Ant or an equivalent!). There are some drawbacks to this approach:
  1. The artifacts you are testing in one environment are not *identical* to the (supposedly same) artifacts in another environment. Since the artifacts have been rebuilt / repackaged, this opens up a 'risk-window' during which something could go wrong. I've personally had this occur when a developer unknowingly built the production release using Java 5 (the application was 1.4 at the time), resulting in a botched production deployment even though testing was successful across all other environments.
  2. A 'search and replace', usually being file & text-based, can be fragile and prone to errors. Perhaps a new configuration file is added, without being added to the search pattern; perhaps there is some valid text that coincidentally matches a replacement pattern.
  3. On a large project with multiple environments, multiple builds can be time and space consuming, essentially multiplying your time/space needs by the number of environments.
  4. Replacement values are now 'hard-coded' into the deployed artifacts, and cannot be adjusted without rebuilding and redeploying (and the inherent risk therein).
The alternate approach documented here is to extend Spring's PropertyPlaceholderConfigurer, introducing the concept of a 'runtime environment' - the RuntimeEnvironmentPropertyPlaceholderConfigurer. At application startup time, the runtime environment is determined using a RuntimeEnvironmentKeyResolver implementation (the default is to look for a system property); from this, the corresponding runtime-specific properties files are located and loaded.

Here is a sample bean definition (borrowed from PropertyPlaceholderConfigurer's JavaDoc):
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
</bean>
...and the addition of a RuntimeEnvironmentPropertyPlaceholderConfigurer bean:
<util:set id="runtimeEnvironments">
<value>production</value>
<value>staging</value>
<value>integration</value>
<value>development</value>
</util:set>

<bean class="digitalascent.framework.support.environment.RuntimeEnvironmentPropertyPlaceholderConfigurer">
<property name="propertyLocation" value="/WEB-INF/runtime-properties/" />
<property name="environments" ref="runtimeEnvironments"/>
<property name="defaultEnvironment" value="development" />
</bean>

Now the properties 'jdbc.driver' and 'jdbc.url' can be defined in the environment specific properties files: WEB-INF/runtime-environment/development.properties, integration.properties, staging.properties and production.properties.

By default, the implementation looks to the system property 'runtime.environment' for the environment key (e.g. -Druntime.environment=production); this property name can be changed (its a property of the bean), or for more advanced uses a custom RuntimeEnvironmentKeyResolver can be wired to to resolve the runtime environment.

The RuntimeEnvironmentPropertyPlaceholderConfigurer also, by default, enables overriding of any properties by system properties; this allows for 'tweaking' the configuration post-deployment, without having to rebuild and redeploy. In the above example, if the hostname of your database server needs to change, you can override the jdbc.url property by adding '-Djdbc.url=newUrlHere' to the relevant startup script.

The source code can be found attached to the Spring JIRA enhancement request.

Some points to note:
  • Source is based on Java 5+; it should be trivial to back-port to Java 1.4 if necessary;
  • The examples are based on Spring 2.0; there is nothing in the code that should prevent use on Spring 1.2.x;
  • If your application has multiple Spring contexts (say a web context and a servlet context), you will need to define the RuntimeEnvironmentPropertyPlaceholderConfigurer bean in each context
Thanks goes to:
  • Peter Monks for providing the use-case / inspiration for the RuntimeEnvironmentKeyResolver;
  • Members of the Spring community for feedback (forum post)
  • Digital Ascent for sponsoring the development of this solution