Saturday, July 26, 2008

GWT Spring Integration

Update: I have recently posted a follow-up blog http://seewah.blogspot.com/2009/02/gwt-spring-integration-using.html which demostrates how to combine this approach with the more conventional method of loading ApplicationContext xml using ContextLoaderListener.

Update 2: I have put together a sample app to demonstrate the integration technique (based on the more conventional method of loading ApplicationContext.xml using ContextLoaderListner): http://seewah.blogspot.com/2009/06/gwt-and-spring-security-sample-demo.html Just take out applicationContext-security.xml if you are not interested in the Spring Security aspect!


This is my first technical post. Sorry about the code formatting. I will fix it later!

A couple of months ago, I started working on a GWT (Google Web Toolkit) project, using Spring as a IoC container to manage service beans on the server-side. An initial Google search for "spring gwt integration" led to a few ideas. Check out http://www.jroller.com/galina/entry/google_web_toolkit_integration_with for a summary of some of the notable contributions out there.

While there are a lot of working (and sometimes quite clever) solutions out there, most tend to tackle the problem in a rather round-about way. So I came up with my own solution, which, in my humble opinion, is quite a bit simpler. It is by no means perfect, but it may serve as an interesting reference for those who are interested.

First of all, I want to leverage the built-in Tomcat provided by GWT, and I want to stick to the GWT model of letting com.google.gwt.dev.shell.GWTShellServlet handle all requests in hosted mode - i.e absolutely no changes to the standard GWT application setup are required to run the app in hosted mode.

I created two classes: DependencyInjectionRemoteServiceServlet and SpringApplicationContextLoader:

DependencyInjectionRemoteServiceServlet extends the standard
com.google.gwt.user.server.rpc.RemoteServiceServlet, and this is going to be the class that all RPC servlet implementions will be extending. For example, if you have a service for authenticating users, you will have two interfaces:



public interface AuthenticationService extends RemoteService {...}

public interface AuthenticationServiceAsync {...}



and at least one implementation class:



public class AuthenticationServiceImpl extends DependencyInjectionRemoteServiceServlet implements AuthenticationService {...}



Before looking at what DependencyInjectionRemoteServiceServlet does, let's take a quick look at SpringApplicationContextLoader first:




import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* Loads Spring application context from the classpath, and provides static
* method to retrieve Spring-managed beans from the loaded context by name.
*
* @author See Wah Cheng
* @created 27 Jun 2008
*/
public class SpringApplicationContextLoader {

private static final String CONTEXT_LOC = "classpath:applicationContext.xml";

private static ApplicationContext applicationContext = new ClassPathXmlApplicationContext(new String[] { CONTEXT_LOC });

/**
* Returns a Spring-managed bean by name.
*
* @param name
* name of bean in the application context
* @return the Spring-managed bean
* @exception NoSuchBeanDefinitionException
* if a suitable bean cannot be found in the Spring application
* context. The current implementation looks up beans by name
*/
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
}



So nothing clever here - just a class to load the Spring applicationContext and to retrieve beans by name.

Now let's look at the DependencyInjectionRemoteServiceServlet code:



import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
/**
* {@link RemoteServiceServlet} that automatically injects IoC dependency.
* "org.springframework.beans.factory.annotation.Autowired" annotation is used
* for marking which fields to inject into. Uses
* {@link SpringApplicationContextLoader} to retrieve beans by name.
* <p>
* Note that the current implementation will only inject "declared" fields, and
* not inherited fields. Fields can be private, protected, package or public.
*
* @author See Wah Cheng
* @created 27 Jun 2008
*/
@SuppressWarnings("serial")
public class DependencyInjectionRemoteServiceServlet extends RemoteServiceServlet {

protected static Logger logger = Logger.getLogger(DependencyInjectionRemoteServiceServlet.class);

public DependencyInjectionRemoteServiceServlet() {
super();
doDependencyInjection();
}

/**
* Carries out dependency injection. This implementation uses Spring IoC
* container.
*
* @exception NoSuchBeanDefinitionException
* if a suitable bean cannot be found in the Spring application
* context. The current implementation looks up beans by name
*/
protected void doDependencyInjection() {

for (Field field : getFieldsToDependencyInject()) {
try {
boolean isFieldAccessible = field.isAccessible();
if (!isFieldAccessible) {
field.setAccessible(true);
}
field.set(this, SpringApplicationContextLoader.getBean(field.getName()));
if (!isFieldAccessible) {
field.setAccessible(false);
}
logger.debug("Dependency injection successful: " + this.getClass().getName() + "." + field.getName());
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

/**
* Find annotated fields to inject.
*
* @return a list of all the annotated fields
*/
private Set<Field> getFieldsToDependencyInject() {
Set<Field> fieldsToInject = new HashSet<Field>();
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.getAnnotation(Autowired.class) != null) {
fieldsToInject.add(field);
}
}
return fieldsToInject;
}
}



So what is happening here? When the servlet instantiates, it calls doDependencyInjection, which uses reflection to find out which fields to inject Spring beans into based on the @Autowired annotation. It will attempt to find the bean in the Spring application context by name using SpringApplicationContextLoader. Note that I am only using @Autowired to mark out fields. It does not do anything else. I could have devised my own annotation, but I have opted to use this Spring annotation for convenience's sake. So all you have to do now, when creating your RPC service implementation, is to extend DependencyInjectionRemoteServiceServlet and annotate your fields:



public class AuthenticationServiceImpl extends DependencyInjectionRemoteServiceServlet implements AuthenticationService {

@Autowired
private UserAccountService userAccountService;

public boolean authenticate(String userName, String password) {
userAccountService.authenticate(userName, password);
....
}
}



and of course in your applicationContext.xml, you have to define a bean whose id is userAccountService.



<bean class=".....UserAccountServiceImpl" id="userAccountService"/>