August 14, 2009

Marrying GWT with Spring the Generic Way

During the last days I've played around with GWT and Spring. Although there are already plenty of blog posts describing how to tunnel GWT Services through a Spring DispatcherServlet, they have all one disadvantage in common: They require to do a lot of explicit Spring Bean Definitions. In this post I describe a nice mechanism to automatically create those Bean Definitions, freeing the Bean Config of redundant, boilerplate code.


Putting Spring into a GWT application is a good idea! Besides several other aspects which make dependency injection a Great Thing, it also simplifies the handling of Service Implementations on the server side. In a vanilla GWT application it is assumed, that each implementation inherits com.google.gwt.user.server.rpc.RemoteServiceServlet, which results in a lot of configuration in your web.xml.
This problem has already been identified other people, for instance Richard Bondi describes how to introduce a org.springframework.web.servlet.DispatcherServlet to solve this discrepancy. This is a good first step, because it allows us to implement our Service Implementations independent of RemoteServiceServlet (Loosely Coupling is always a good idea, esp. with respect to testability), but it just shifts redundant configuration from the former web.xml to our Spring Bean Configuration.
If i may quote the above cited article, we end up with:

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
       <property name="mappings">
         <value>
         /**/login.whatever=loginController
         /**/userProfile.whatever=userProfileController
         </value>
       </property>
     </bean>

     <bean name="loginController" class="com.aspentech.imos.servlet.GWTController">
       <property name="remoteService">
         <bean class="com.foo.gwt.login.server.LoginServiceImpl" />
       </property>
     </bean>

     <bean name="userProfileController" class="com.aspentech.imos.servlet.GWTController">
       <property name="remoteService">
         <bean class="com.foo.gwt.login.server.UserProfileService" />
       </property>
     </bean>
Do you see what i am talking about? Defining the controller beans is just boilerplate, defining the mapping is redundant. This might be anything, but it is surely not DRY. Could we improve it? I think yes, let me show you how.

Prerequisites

  • There is a GWT application.
  • We have a GwtController as described by Richard Bondi.

Annotating the Service Implementation

My idea is to
  1. Automatically find those classes which are supposed to be service implementations.
  2. Automatically create the entries of the SimpleUrlHandlerMapping.
Finding some classes at runtime is a proper use case for Annotations. We mark those classes with a distinct Annotation, and search them on the classpath at runtime. If we add some configuration values, like service endpoint name, we could also create the url handler mapping. Let's see what GWT already gives us:
@RemoteServiceRelativePath("greeting.service")
    public interface GreetingService extends RemoteService {
     String greetServer(String name); 
    }  
Ok, we have an Annotation and yes, oha, it defines (a part of) the Service Endpoint. No, it is not our implementation, but only the pure service interface. Maybe we could use this, then search for classes implementing this interface ... arg, hang on ..., if we look closer, we see that @RemoteServiceRelativePath is actually of RetentionPolicy.CLASS, which in turn means it is not available in the VM at runtime. Damn, as long as this is not changed, we cannot use the Annotation.
Nonetheless, doing the job with Annotations is still a good idea. Hence the second best solution is writing our own:
@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface RemoteServiceImplementation {
     String value();
    }
Which is the applied to our Service Implementation
@RemoteServiceImplementation("/**/greeting.service")
    public class GreetingServiceImpl implements
      GreetingService {

     public String greetServer(String input) {
      return "Hello, " + input + ". How are you?";
     }
    }
As you see, the Annotation value is our endpoint mapping entry. Yes, this violates DRY, but maybe we have to accept it for now.

Searching for Annotations

Bill Burke describes very detailed the problems with respect to scanning for Annotations on the classpath. His consequence is Scannotation, a small framework for looking up annotated classes without bloating the VM's Permanent Generation. He forgets to mention that Scannotation depends on javassist, but now u are warned.
As a matter of fact I've encapsulated this scanning behind a strategy to enable better testing, it finally looks like
import java.io.IOException;
    import java.util.Collection;
    
    public interface AnnotationScanner {

     Collection< String > scanClassPathForAnnotation(String annotationName)
     throws IOException;
    }
And
import java.io.IOException;
    import java.net.URL;
    import java.util.Collection;
    
    import javax.servlet.ServletContext;

    import org.scannotation.WarUrlFinder;
    import org.scannotation.AnnotationDB;
        
    import org.springframework.web.context.ServletContextAware;

    public abstract class WarAnnotationScanner 
    implements AnnotationScanner,ServletContextAware {
        
        private ServletContext servletContext;

     public void setServletContext(ServletContext servletContext) {
      this.servletContext = servletContext;  
     }

     private URL createClasspath() {
      if(servletContext==null) throw new NullPointerException();
      return WarUrlFinder.findWebInfClassesPath(servletContext);
     }

     public Collection scanClassPathForAnnotation(
             String annotationName) throws IOException {

      URL url = createClasspath();
      AnnotationDB annotationDB = new AnnotationDB();

      annotationDB.setScanClassAnnotations(true);
      annotationDB.setScanFieldAnnotations(false);
      annotationDB.setScanMethodAnnotations(false);
      annotationDB.setScanParameterAnnotations(false);

      annotationDB.scanArchives(url); 

      return annotationDB.getAnnotationIndex().get(annotationName); 
     }
    }

Automatically Creating Bean Definitions

How do we do this? Again, there is a good article by Carlo Bonamico about this topic. Similar to him, we will also implement org.springframework.beans.factory.config.BeanFactoryPostProcessor, since we need to get some other classes inject. The drawback of this approach is, that we have to make a downcast of ConfigurableListableBeanFactory to BeanDefinitionRegistry. This is a violation of OCP, but again we don't get around it. The main idea of the ServiceUrlMappingRegistrationBean is to generate all those Spring Beans we mentioned above programmatically.
import java.io.IOException;
import java.util.Collection;
import java.util.Map;

import org.apache.log4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;

import com.google.common.collect.Maps;

public class ServiceUrlMappingRegistrationBean 
implements BeanFactoryPostProcessor  {
 
 private static final Logger log = Logger.getLogger(ServiceUrlMappingRegistrationBean.class);
 
 private Class controllerClass;

 private AnnotationScanner annotationScanner;
 
 public void setAnnotationScanner(AnnotationScanner annotationScanner) {
  this.annotationScanner = annotationScanner;
 }
 
 public void setControllerClass(String controllerClassName) 
 throws ClassNotFoundException {
  this.controllerClass = 
   getClass().getClassLoader().loadClass(controllerClassName);
 }

 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
   throws BeansException {
  try {
   
   final BeanDefinitionRegistry registry = 
    (BeanDefinitionRegistry) beanFactory;   
   final Map mapping = Maps.newHashMap();
      
   for(String className : scanClassPathForRemoteServices()) {
    
    final String beanName = toBeanName(className);
    final Class remoteServiceclass = 
     getClass().getClassLoader().loadClass(className);    
    registry.registerBeanDefinition(
      beanName,createServiceBeanDefinition(remoteServiceclass));
    
    final String servicePath = extractServicePath(remoteServiceclass);
    mapping.put(servicePath,beanName);
    
    log.debug("Register " + className + " with " 
      + servicePath + "=" + beanName);
   }
   registry.registerBeanDefinition(
     "urlMapping", createUrlMappingBeanDefinition(mapping));
   
  }catch(IOException e){
   log.error("Could not scan annotated classes.",e);
  }catch(ClassNotFoundException e){
   log.error("Class not found.",e);
  }
 }
 
 private String extractServicePath(Class clazz){
  RemoteServiceImplementation annot = 
   clazz.getAnnotation(RemoteServiceImplementation.class);
  return annot.value();
 }
 
 private String toBeanName(String className){
  return className;
 }
 
 private BeanDefinition createServiceBeanDefinition(
   Class remoteServiceclass) throws ClassNotFoundException{
  
  RootBeanDefinition beanDef = new RootBeanDefinition(this.controllerClass);  
  RootBeanDefinition remoteServiceBean =
   new RootBeanDefinition(remoteServiceclass);
  
  BeanDefinitionHolder holder =
   new BeanDefinitionHolder(remoteServiceBean, "");
  
  MutablePropertyValues values = new MutablePropertyValues();
  values.addPropertyValue("remoteService", holder);
  beanDef.setPropertyValues(values);
  
  return beanDef;
 }
 
 private BeanDefinition createUrlMappingBeanDefinition(
   Map mapping){
  RootBeanDefinition beanDef = 
   new RootBeanDefinition(SimpleUrlHandlerMapping.class);
 
  MutablePropertyValues values = new MutablePropertyValues();
  values.addPropertyValue("mappings",mapping);  
  beanDef.setPropertyValues(values);
  return beanDef;
 }
 
 private Collection< String > scanClassPathForRemoteServices() 
 throws IOException{
  if(annotationScanner == null) 
   throw new NullPointerException("AnnotationScanner");
  
  String annotName = RemoteServiceImplementation.class.getName();    
  return annotationScanner.scanClassPathForAnnotation(annotName);  
 }

}
Which finally ends up in the following Bean Definition
<bean class="raidr.server.spring.ServiceUrlMappingRegistrationBean">
  <property name="controllerClass"
            value="raidr.server.servlet.GWTController" />
  <property name="annotationScanner">
            <bean class="raidr.server.spring.WarAnnotationScanner"/>
        </property> 
 </bean>
Isn't it beautiful?

Finally

In this article we described a mechanism to automatically create bean definitions for GWT Services dispatched by the DispatcherServlet by adding some homegrown Annotations. There are two known limitations: We duplicate Service End Point Description in the Annotations and we violate OCP by making a downcast to the BeanDefinitionRegistry.

8 comments:

Unknown said...

Hi,

It's scary as this is exactly the approach I took in GWToolbox. The springrpc module holds the code base and it also comes with a namespace for the configuration. I encountered the same issue with the RetentionPolicy so instead I'm using Spring's RequestMapping annotation. Checkout http://code.google.com/p/gwtoolbox/source/browse/#svn/trunk/modules/springrpc/src/main/java/org/gwtoolbox/springrpc

Lars Girndt said...

@boness

Yes, indeed this is scary. Please let me emphasize, that I've not seen your code before. But then it's even more clear, that this solution seems to be obviously a quite good one :-)

On the other hand, it's really no magic and hence very straight forward.

Unknown said...

Don't worry... I didn't think you "stole" my code... it was just funny to read it from someone else :-). But yeah... I share your thoughts about it and I do think it's the right way to go. I'll work toward releasing this as a downloadable jar so more people can use it. Keep up the good postings...

cheers,
Uri

Ralph said...

Hi

Thanks for this interesting post.

But compared to the way spring4gwt does the connection, this looks a bit complicated.
http://code.google.com/p/spring4gwt/wiki/SimpleRPCExample

With spring4gwt I only have to include a servlet definition in web.xml and annotate the service with a normal @Service annotation.

Isn't that the better way. Or is there something that speaks against this approach?

Lars Girndt said...

Hi Ralph,

Meanwhile I get the bad feeling that I've not done my homework very well ;) - obviously there are already plenty of existing solutions.

Nonetheless, yes, after having a quick look at spring4gwt i need to admit, if it works, then it is surely an even better approach then my proposed solution.

Although it would be interesting to see, how they manage to create the full end point name for the service. This was something, i have not accomplished.

Anonymous said...

...........................................................................

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...
This comment has been removed by a blog administrator.