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<String, RemoteService> springRPCServices = new HashMap<String, RemoteService>();

	@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<String, RemoteService>) 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 can be found on Github at StockWatcherRPC

 
 

About these ads

About DuyHai DOAN
Java freelancer LinkedIn profile : http://fr.linkedin.com/pub/duyhai-doan/2/224/848 Follow me on Twitter: @doanduyhai

12 Responses to GWT RPC integration with Spring

  1. Pingback: GWT JSON integration with Spring MVC « DuyHai's Java Blog

  2. Joe Weiner says:

    DuyHai-

    Nicely done- very helpful for the GWT newbie. Quick question from your experience:

    In the GWT sample code from Google, the RemoteService implementation is also an extension of RemoteServiceServlet, so you have access to inherited methods from this class and its supers in your service implementation. This is required for many things you might want to access in your service implementation, such as headers, session, context, etc.

    Was thinking (have not tried) of extending the RemoteService interface to include something like setServlet(HttpServlet), which could be called as necessary just before RPC.invokeAndEncodeResponse() in your code above. Your RemoteServices would then of course implement this extended interface and regain access to servlet methods.

    Thoughts?

    Thanks,
    -Joe

    • doanduyhai says:

      Hello Joe

      Of course you can extend the RemoteService with some methods of your own. For example you can define MyRemoteService to extends generic RemoteService with the extra method setServlet(HttpServlet)

      Doing that, in the processCall(String payload) method execution flow, you can set the HttpServlet to your service before RPC.invokeAndEncodeResponse() is called.

      Do not forget to change the signature of the springRPCServices field from Map<String, RemoteService> to Map<String, MyRemoteService>

  3. You are so cool! I don’t believe I’ve truly read through something like
    that before. So good to discover another person with a few unique thoughts on this
    subject matter. Really.. thanks for starting this up. This website
    is one thing that is required on the web, someone with a bit of originality!

    • DuyHai DOAN says:

      Thanks for appreciating.

      I’ve stopped using GWT for a while. The new trend for Web development is now pure HTML5 + JS frameworks (AngularJS, EmberJS …) for the front end and pure server technos (Java, Ruby, Node.js) for the back-end.

      The time we generate web pages from server side is gone. Frameworks like JSF, GWT, Spring MVC.. are going nowhere

  4. Pingback: GWT JSON integration with Spring MVCJava Beginners Tutorial

  5. Great article about spring – gwt integration. Your solution is elegant and easy to apply. One thing you shouldn’t noticed is that HashMap isn’t appropriate for multithreaded applications. Instead you should use ConcurrentHashMap in order to ensure both thread safety and safe publication.

  6. These are truly fantastic ideas in concerning blogging.
    You have touched some fastidious factors here. Any way keep up wrinting.

  7. PS4 Emulator says:

    Hi! I could have sworn I’ve been to this web site before but after browsing through many of
    the articles I realized it’s new to me. Regardless, I’m definitely delighted I found
    it and I’ll be bookmarking it and checking back frequently!

  8. It’ll be easier too do that iff the two of you are still living under
    the same roof. As soon as Sandra Bullock found outt that Jesse James was cheating on her shhe bolted,
    not even answering any of his calls. As a result of this, numerous people literally refer to all of
    them as cheats, so avoid getting mixed up from
    this.

  9. Also, it only takes 2 days too harvest acai berry
    versus 5 days for pomegranates. I’ve noticed playing the game and checking out heaps of
    other people’s farms that colour and order seems to be more prefterred as does themes of things.
    Well, Cleaning it once a, but the wily fox had deleted every message she didn.

  10. Show your wisdom and act little bit miserly in this case.
    Italy vs Spain 2-1 All Goals & Match Highlights (International Friendly).
    Let’s start with presentation, FWCSA has some new tricks that actually make it
    better than FIFA 10.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 39 other followers

%d bloggers like this: