Exposing Spring Services for consumption by GWT Front End

This is a summary of the communication methods I’ve employed in an application at one of our clients recently, methods gathered from various other sites, blogs, etc.

 

I’ll start with some basic terminology -

 

MVC Controller:

 

          Pure client-side component, written in Java and translated to JavaScript. Takes part in the Model-View-Controller flow by listening to application events typically generated by GUI widgets, issuing http requests to the controller layer and passing back the results, again usually via custom application events.

 

Controller:

 

          Server side component intercepting client side requests and typically delegating them to the service layer. Acts as a thin mediation layer between the client side components and the business logic. Controllers can either be handwritten or act in a generic manner and used as transparent proxies to the services.

 

Service:

 

          A Class implementing some business logic, typically communicating with the backend vialocal calls, web service calls, RMI calls, or other. Services are nowadays usually declared using Spring annotations and be picked up by Spring's annotation detection mechanism once, during web application startup.

 

From here on, I will present 3 techniques for setting up communication between MVC controllers and application services:

 

 

1. GWT  Transport/Serialization
(based on http://www.oktech.hu/kb/spring-gwt-integration-with-ease)

 

This method should be employed as default for invocation/serialization mechanism, being fast and native to GWT. Here, instead of writing GWT servlets, we use the power of Spring for detection and auto-wiring of the services.

 

Configuration

 

1.1 WEB-INF/web.xml servlet definitions (here only showing snippets which apply to gwt-rpc-style services):

 

    <servlet>

        <servlet-name>spring-rpc</servlet-name>

        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <load-on-startup>1</load-on-startup>

    </servlet>

    <servlet-mapping>

        <servlet-name>spring-rpc</servlet-name>

        <url-pattern>/my-app/rpc/*</url-pattern>

    </servlet-mapping>

 

These definitions set up a Spring dispatcher servlet picking up all services that should be exposed as gwt-rpc

 

1.2 Setting up WEB-INF/spring-rpc-servlet.xml containing generic Spring Web definitions of handler-mapping (mapping between incoming requests and controllers) and Handler Adapter, responsible for the actual bridging between GWT and Spring.

 

<?xml version="1.0" encoding="UTF-8"?>

<beans ...>

      <context:component-scan base-package="com.mycompany.myapp" />

      <bean class="com. mycompany.myapp.server.GwtServiceHandlerAdapter"/>

      <bean class="com. mycompany.myapp.server.GwtServiceHandlerMapping">

            <property name="prefix" value=""/>

            <property name="suffix" value=""/>

      </bean>

</beans>

 

…to get a better insight of the mechanism, take a look at GwtServiceHandlerAdapter class:

 

@Override

public String processCall(final String payload) throws SerializationException {

try {

              Object handler = handlerHolder.get();

              final RPCRequest rpcRequest = RPC.decodeRequest(

payload, handler.getClass(), this);

 

return RPC.invokeAndEncodeResponse(handler, rpcRequest.getMethod(),

rpcRequest.getParameters(), rpcRequest.getSerializationPolicy());

}

catch (final IncompatibleRemoteServiceException ex) {

getServletContext().log("Exception was thrown while processing this call.", ex);

              return RPC.encodeResponseForFailure(null, ex);

       }

}

 

the first 2 parts are generic configuration which is not specific to any controller. From here on starts specific per-service code

 

1.3 Sample Service interface:

 

@RemoteServiceRelativePath("rpc/tray")

public interface DashboardTrayController extends RemoteService {

       TrayModel retrieve(TrayQueryConfig trayConfig);

     

}

 

Note, that for the mechanism to work, there must be correspondence between the @RemoteServiceRelativePath's first path element (namely, 'rpc') and the servlet-mapping's (in the above web.xml) last part. The annotation, by the way, is used for GWT's clientside proxy generation and results in the following code in DashboardTrayControllerAsync.java (courtesy of gwt-maven-plugin's code generation):

 

public static final DashboardTrayControllerAsync getInstance() {

if ( instance == null ) {

instance = (DashboardTrayControllerAsync) GWT.create(

DashboardTrayController.class );

 

ServiceDefTarget target = (ServiceDefTarget) instance;

        target.setServiceEntryPoint( GWT.getModuleBaseURL() + "rpc/tray" );

}

return instance;

}

 

Also please note, we are using GWT's RemoteService as a base interface both to satisfy GWT's requirement but also to enable the above-mentioned async interface generation by gwt-maven-plugin.

 

 

2. Automated JSON Serialization (based on Spring/Jackson intergration)

 

This solution is suitable for situations where API need to be externalized over REST/JSON and still be consumed by the application's clientside. Note: when consuming client is GWT code, one may use one of the existing JSON decoding techniques, for example see: http://code.google.com/p/google-web-toolkit-doc-1-5/wiki/GettingStartedJSON. In our application, the only consumers of JSON data are GXT widgets that are directly wired to the controller's endpoints and so no such decoding is required.

 

Configuration

 

2.1 In WEB-INF/web.xml, we define another entry of the DispatcherServlet, this time mapped to a different URL:

 

<servlet>

<servlet-name>spring-rest</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

 

<servlet-mapping>

<servlet-name>spring-rest</servlet-name>

<url-pattern>/my-app/rest/*</url-pattern>

</servlet-mapping>

 

2.2 Spring's context descriptor (WEB-INF/spring-rest-servlet.xml) now contains Spring/Jackson definitions that are generic to all controllers deployed as REST/JSON endpoints:

 

<?xml version="1.0" encoding="UTF-8"?>

<beans ...>

       <context:component-scan base-package="com. mycompany.myapp" />

 

  <bean id="handlerMapping" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />

  

  <bean id="handlerAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

 

       <bean

              class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"

              p:order="1">

              <property name="mediaTypes">

                     <map>

                           <entry key="json" value="application/json" />

                     </map>

              </property>

              <property name="defaultViews">

                     <list>

                           <bean class="com.mycompany.myapp.server.ObjectJacksonJsonView">

                                  <property name="disableCaching" value="true"/>

                                  <property name="useImplicitRoot" value="true"/>

                           </bean>

                     </list>

              </property>

       </bean>

</beans>

 

In order to be able to slightly customize the generated JSON, I had to extend the class MappingJacksonJsonView provided by Spring, adding the useImplicitRoot property that allows trimming the root JSON element, for better digestion by out client-side widgets. My override to the renderMergedOutputModel(Map<String, Object> model, HttpServletRequest req, HttpServletResponse res)  method included the following logic:

 

if(useImplicitRoot) {

Map valueMap = (Map)value;

if(valueMap.size() == 0) {

objectMapper.writeValue(generator, value);

       }

       else if(valueMap.size() == 1) {

objectMapper.writeValue(generator, valueMap.values().iterator().next());

       }

else {

              throw new IllegalArgumentException(

"cannot serialize multiple objects when useImplicitTree set to true");

       }

}

else  {

objectMapper.writeValue(generator, value);

}

 

2.3 Controller Code

 

The controller interface has no special requirements in this case so we will skip it; as for the controller implementation, it is a standard Spring Web controller, which may look like this:

 

@RequestMapping("/tag-suggest")

@Controller

public class TagSuggestControllerImpl implements TagSuggestController {

 

       private static final int MAX_LIMIT = 50;

private static final Log logger =

LogFactory.getLog(TagSuggestControllerImpl.class);

      

       @Autowired

       private MyService svc;

 

       @Override

       @RequestMapping(value = "/get", method = { RequestMethod.GET })

       public TagList getAvailableTags(@RequestParam String query, @RequestParam int limit,

@RequestParam String dictionary) {

              // …

       }

       //...

}

 

The class's RequestMapping annotation is configured to "/tag-suggest", meaning that in order to externally invoke any of the methods in this controller, a prefix of "/my-app/rest/" is needed (if you are not sure why, check web.xml above). In addition, for every public method in this type of controller, a @ReuqestMapping annotation is required, specifying the remaining url, including query/path parameters etc. For full documentation regarding web controller methods annotations, parameter and request object binding, see: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/mvc.html.

 

2.4 Customizing the returned JSON

 

At runtime, the value returned from every mapped method (a.k.a "web-method" in Microsoft jargon) is automatically converted to JSON by the converters specified in spring-rest-servlet.xml, which we reviewed earlier. The conversion is done according to the JavaBean default standards, which might, in some cases, just not cut it. In these caesars, you may customize the returned JSON by annotating the value objects themselves, in a manner similar to:

 

@JsonUseSerializer(TagCollectionSerializer.class)

public class TagList {

       //...

}

This annotation is Jackson specific, which is a sad leakage of layer logic, but cannot be helped. The class specified as the annotation value should implement the JsonSerializer interface (which is, again, Jackson-specific) and implement the main serialization method: serialize(Object value, JsonGenerator gen, SerializerProvider provider). For example, consider the following relatively simple implementation:

 

@Override

public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider)

throws IOException, JsonProcessingException {

 

       if(value == null) {

              provider.getNullValueSerializer().serialize(null, jgen, provider);

       }

       else if(!value.getClass().isAssignableFrom(TagList.class)) {

              throw new IllegalArgumentException(

"not a TagList instance: " + value.getClass());

       }

       else {

              TagList tags = (TagList)value;

              jgen.writeStartObject();

              jgen.writeNumberField("total", tags.getTotal());

              jgen.writeArrayFieldStart("results");

              for(String tag : tags.getTags()) {

                     if(tag == null) {

                           jgen.writeNull();

                     }

                     else {

                           jgen.writeStartObject();

                           jgen.writeStringField("tag", tag.toString());

                           jgen.writeEndObject();

                     }

              }

              jgen.writeEndArray();

              jgen.writeEndObject();

       }

}

 

 

 

3. Raw Content Generation

 

A third option, relevant in cases where JSON content is brought from a third party and should be conveyed to the client as-is, or where massive customization of the value object is impractical is writing Spring controllers that generate the actual JSON and do not take advantage of Spring/Jackson's automatic serialization mechanism.

 

Configuration

 

3.1 Again, we start by defining another DispatcherServlet in web.xml:

 

<servlet>

        <servlet-name>spring-raw</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

 

<servlet-mapping>

<servlet-name>spring-raw</servlet-name>

<url-pattern>/my-app/raw/*</url-pattern>

<url-pattern>/my-other-app/raw/*</url-pattern>

</servlet-mapping>

 

3.2 This time, our Spring configuration file (WEB-INF/spring-raw-servlet.xml) will not contain any special definitions, since we will be using Spring MVC's defaults:

 

<?xml version="1.0" encoding="UTF-8"?>

<beans ...>

           <context:component-scan base-package="com. mycompany.myapp" />

</beans>

 

3.3 Same as in previous scenario, we define a no brainer Controller interface and provide its implementation:

 

@Controller

@RequestMapping("/info-items")

public class InfoItemsControllerImpl implements InfoItemsController {

 

       @Autowired

       private InfoItemsQueryManager infoItemsQueryManager;

      

       @Autowired

       private QueryContextAssembler contextAssembler;

 

       @ResponseBody

       @RequestMapping(value = "/get/{docId}", method = { RequestMethod.GET })

       public String get(@PathVariable String docId, @RequestParam String dictionary)

throws IOException {

              //...

       }

       //...

}

 

The only new thing worth noticing here is the @ResponseBody annotation on the web method. This (Spring-)standard annotation tells Spring MVC to transmit the value returned from the method as-is. Remember this option should be used sparingly, even if it gets you to your end-goal in the shortest time, it involves writing more and more JSON code, creating an undesired relationship between your web-oriented business logic implementation and the serialization mechanism.

 

Developer