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- Automatically find those classes which are supposed to be service implementations.
- Automatically create the entries of the
SimpleUrlHandlerMapping
.
@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 onjavassist
, 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 CollectionscanClassPathForAnnotation( 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 implementorg.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 MapWhich finally ends up in the following Bean Definitionmapping = 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); } }
<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?
8 comments:
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
@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.
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
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?
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.
...........................................................................
Post a Comment