GWT RPC integration with Spring

Recently I started studying GWT, a new web framework for my curriculum. The main idea of GWT is to let you code the GUI part in Java (with static compilation and type checking) then translate this code into Javascript.

For the backend, GWT relies on an RPC system using plain old Servlet. Each RPC service is published as a distinct servlet so you end up having as many servlets are there are distinct RPC services.

Not only this approach is not optimal for resources management server-side wise, it also pollutes your web.xml with many servlet declarations.

Ideally we should have an unique servlet serving as a router and dispatching RPC calls to appropriate service beans managed by Spring. Spring is an appropriate framework for managing business in the backend due to its industry-wide adoption and its mature extensions portfolio. Needless to say that this design can be easily adapted for JEE containers too.

Disclaimer: the code presented below has been inspired from projects like spring4GWT and gwtrpc-spring. I took the same approach and modified the URL mapping part so the original credits go for them.

I SpringRPCDispatcherServlet

RPC processing with GWT not only consist of calling the appropriate service in the backend. It also involves some plumbing tasks like object serialization/deserialization to pass parameters back and forth. Instead of re-inventing the wheel and re-coding everything from scratch, it is wiser to re-use the existing GWT infrastructure and extends the RemoteServiceServlet class to adapt it to our needs.

public class SpringRPCDispatcherServlet extends RemoteServiceServlet
{
	private static final long serialVersionUID = 1L;
	private static final Log logger = LogFactory.getLog(SpringRPCDispatcherServlet.class);
	private static final String SERVICE_URL_MAPPER = "serviceURLMapper";
	private static final UrlPathHelper pathHelper = new UrlPathHelper();

	private WebApplicationContext applicationContext;
	private Map springRPCServices = new HashMap();

	@Override
	public void init()
	{
		applicationContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		if (applicationContext == null)
		{
			throw new IllegalStateException("No Spring web application context found");
		}

		if (StringUtils.isEmpty(this.getInitParameter(SERVICE_URL_MAPPER)))
		{
			throw new IllegalArgumentException("The servlet SpringRPCDispatcherServlet should have a '" + SERVICE_URL_MAPPER + "' parameter defined");
		}
		else
		{
			String beanName = this.getInitParameter(SERVICE_URL_MAPPER);
			this.initServiceURLMapper(beanName);
		}
		logger.info("SpringRPCDispatcherServlet deployed");
	}

	@SuppressWarnings("unchecked")
	private void initServiceURLMapper(String beanName)
	{

		this.springRPCServices = (Map) applicationContext.getBean(beanName, Map.class);
	}

	@Override
	public String processCall(String payload) throws SerializationException
	{
		try
		{
			RemoteService handler = retrieveSpringBean(getThreadLocalRequest());
			RPCRequest rpcRequest = RPC.decodeRequest(payload, handler.getClass(), this);
			onAfterRequestDeserialized(rpcRequest);
			if (logger.isDebugEnabled())
			{
				logger.debug("Invoking " + handler.getClass().getName() + "." + rpcRequest.getMethod().getName());
			}
			return RPC.invokeAndEncodeResponse(handler, rpcRequest.getMethod(), rpcRequest.getParameters(), rpcRequest.getSerializationPolicy());
		}
		catch (IncompatibleRemoteServiceException ex)
		{
			logger.error("An IncompatibleRemoteServiceException was thrown while processing this call.", ex);
			return RPC.encodeResponseForFailure(null, ex);
		}
	}

	protected RemoteService retrieveSpringBean(HttpServletRequest request)
	{
		String serviceURL = extractServiceURL(request);
		RemoteService bean = getBeanByServiceURL(serviceURL);

		if (logger.isDebugEnabled())
		{
			logger.debug("Bean for service " + serviceURL + " is " + bean.getClass());
		}
		return bean;
	}

	private String extractServiceURL(HttpServletRequest request)
	{
		String service = pathHelper.getPathWithinServletMapping(request);
		if (logger.isDebugEnabled())
		{
			logger.debug("Service for URL " + request.getRequestURI() + " is " + service);
		}
		return service;
	}

	private RemoteService getBeanByServiceURL(String serviceURL)
	{
		if (!this.springRPCServices.containsKey(serviceURL))
		{
			{
				throw new IllegalArgumentException("Spring bean not found for service URL: " + serviceURL);
			}
		}
		return this.springRPCServices.get(serviceURL);
	}

Let’s analyze the code step-by-step.

As private fields, we declare a UrlPathHelper (line 6) to extract paths from URL (necessary to determine which service bean to call) and a map linking a serviceURL to the appropriate Spring bean (line 9).

First in the init() method (line 12) we get a reference on the current Spring Web application context. We check for the presence of the “serviceURLMapper” servlet parameter. If it’s set we initialize the map of serviceURL by calling the method initServiceURLMapper() (line 27). This method simply retrieve the map from Spring context.

At runtime, when a request is sent to the servlet, the processCall(String payload) method is called. This method is copied from the original RemoteServiceServlet class and I introduced some modifications.

  • retrieveSpringBean() is called to get the appropriate Spring service bean for the current request (line 44)
  • retrieveSpringBean() first calls extractServiceURL() which relies on the UrlPathHelper class to extract the path from servlet context (line 74)
  • then a lookup is performed by getBeanByServiceURL() in the springRPCServices map to retrieve the correct Spring bean to service the request

Please notice that the UrlPathHelper is of great help here to extract the path excluding the leading servlet context. The Javadoc of this class gives some examples to clarify its operation:

servlet mapping = “/test/*”; request URI = “/test/a”, result = “/a”
servlet mapping = “/test”; request URI = “/test”, result = “”
servlet mapping = “/*.test”; request URI = “/a.test”, result = “”

 

Now that we’ve created the servlet, let’s see how it is configured with GWT

 

II Configuration

A web.xml

A typical web.xml configuration for the SpringRPCDispatcherServlet is

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

  <!-- Spring classical web application context declaration -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

   <!-- SpringRPCDispatcherServlet declaration -->
  <servlet>
    <servlet-name>springBackendServletDispatcher</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher_rpc.utils.SpringRPCDispatcherServlet</servlet-class>
    <init-param>
      <param-name>serviceURLMapper</param-name>
      <param-value>serviceURLMapper</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <!-- SpringRPCDispatcherServlet servlet mapping -->
  <servlet-mapping>
    <servlet-name>springBackendServletDispatcher</servlet-name>
    <url-pattern>/stockWatcherRPC/rpc/*</url-pattern>
  </servlet-mapping>
...
</web-app>

The first part of this web.xml is a classical Spring web application context declaration, nothing special.

Next we define our special servlet, providing the serviceURLMapper parameter. This value should point to an existing bean id in the Spring context (see below) (lines 17-18).

Finally we associate this servlet with the url pattern “/stockWatcherRPC/rpc/*” (line 26). Only RPC calls using this pattern will be handled by our custom servlet.

 

B applicationContext.xml

Below is the declaration of Spring beans servicing RPC requests from the GUI:

<bean id="stockPriceRPCService" class="com.google.gwt.sample.stockwatcher_rpc.service.StockPriceServiceImpl"/>

<bean id="randomizeRPCService" class="com.google.gwt.sample.stockwatcher_rpc.service.RandomizeServiceImpl"/>

<util:map id="serviceURLMapper" key-type="java.lang.String" value-type="com.google.gwt.user.client.rpc.RemoteService">
	<entry key="/stockPrices" value-ref="stockPriceRPCService"/>
	<entry key="/randomize" value-ref="randomizeRPCService"/>
</util:map>

 

The “serviceURLMapper” is just a map with key of String type representing the serviceURL and value of com.google.gwt.user.client.rpc.RemoteService type representing the corresponding Spring service bean. All server-side services for GWT RPC should implement an user-defined interface which implements itself the generic RemoteService from the GWT SDK so setting it as a supertype for map value type is fine. The map bean is quite straightforward, associating a service path with a declared bean. Please note that in the above example:

        • stockPriceRPCService will service all incoming RPC requests with URL /stockWatcherRPC/rpc/stockPrices

 

        • randomizeRPCService will service all incoming RPC requests with URL /stockWatcherRPC/rpc/randomize

 

Please note that the service URL is the map is the absolute URL removed of the servlet path defined earlier in web.xml (/stockWatcherRPC/rpc/*)

C GWT configuration

From the GWT side the configuration is quite simple: StockWatcherRPC.gwt.xml:

<module rename-to="stockWatcherRPC">

com.google.gwt.sample.stockwatcher_rpc.client.StockPriceService

@RemoteServiceRelativePath("rpc/stockPrices")
public interface StockPriceService extends RemoteService

com.google.gwt.sample.stockwatcher_rpc.client.RandomizeService

@RemoteServiceRelativePath("rpc/randomize")
public interface RandomizeService extends RemoteService

 

D URL mapping summary

Below is a picture summarizing the way URL are handled by our servlet.

GWT_RPC_integration_Spring_URL_Mapping

Please notice that the GWT module name (here stockWatcherRPC) should be included in the servlet mapping url. Most of RPC issues come from erronous service URLs.

Another remark about URL path segregation. We could have declare the Spring servlet mapping as /stockWatcherRPC/* and the url in the map as /rpc/stockPrices.

However associating the Spring servlet mapping to the root of GWT module name (/stockWatcherRPC) is not a good idea because any request from the GUI to retrieve static resources (images, JS files…) will trigger our custom servlet, which we do not want.

That’s why it’s a better practice to separate RPC call with a dedicated URL pattern (here rpc/*)

E Alternate URL pattern

In the above example, we use the /rpc pattern in the URL string to distinguish between client RPC calls and normal resources requests.

But another approach for RPC URL mapping is possible. What if we distinguish all RPC calls with a particular extension, like *.rpc ?

Spring servlet mapping:

<servlet-mapping>
  <servlet-name>springBackendServletDispatcher</servlet-name>
  <url-pattern>/*.rpc</url-pattern>
</servlet-mapping>

Spring service URL map

<util:map id="serviceURLMapper" key-type="java.lang.String" value-type="com.google.gwt.user.client.rpc.RemoteService">
	<entry key="stockPrices.rpc" value-ref="stockPriceRPCService"/>
	<entry key="randomize.rpc" value-ref="randomizeRPCService"/>
</util:map>

 

GWT_RPC_integration_Spring_Alternate_URL_Mapping In this case, we just need to modify our servlet method extractServiceURL() to extract the trailing part with .rpc extension in the absolute URL with an RegExp:

private String extractServiceURL(HttpServletRequest request)
{
	String service = request.getRequestURI().replaceFirst("^.*/([^/]+."+this.rpcExtension+")$", "$1");
	if (logger.isDebugEnabled())
	{
		logger.debug("Service for URL " + request.getRequestURI() + " is " + service);
	}
	return service;
}

this.rpcExtension represents the URL extension for RPC calls (.rpc here). It should be injected in the servlet as init parameter.

III Demo

As a sample application, I took the original StockWatcher from Google tutorial and modified it to use Spring RPC beans in the backend.

GWT_RPC_integration_Spring_Demo

The main “Add” button is calling the stockPrices service. I enriched the GUI with a “Randomize” button and introduced a new randomize service to demonstrate the servlet URL mapping feature.

All source code for this project cn be found on Github at StockWatcherRPC

0 Comments GWT RPC integration with Spring

  1. Funnygamesonline

    Wow, that’s what I was looking for, what a information!
    present here at this web site, thanks admin of this
    website.

    Reply

Leave A Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.