What makes GWT more interesting is that it can be integrated with technologies like Struts, Spring, Hibernate, Ant, Maven etc and this is quite a wonder considering the speed of entreprise applications development.
When I first started this investigation I didn't find very many resources on how these technologies can be integrated and this is how I decided to help the community of those willing to make a step towards this integration. The purpose of this blog is to explain as simple as possible how GWT and Spring can be brought together and make the the most of your entreprise application.
Let's get to the real stuff ;-)
First thing to consider is the directories structure. Pay an extra attention to this step as you will face some difficulties in compiling the files if the sources are not in the right packages.
./src/com/MyApplication.gwt.xml
./src/com/public/MyAplication.htm
./src/com/client/MyApplication.java
./src/com/client/MyService.java
./src/com/client/MyServiceAsync.java
./src/com/server/MyServiceImpl.java
./src/com/server/ServletWrappingController.java
./src/spring-servlet.xml
./src/web.xml
./WEB-INF/lib/spring.jar
./WEB-INF/lib/gwt-user.jar
./WEB-INF/lib/gwt-dev-windows.jar
./WEB-INF/lib/gwt-servlet.jar
./WEB-INF/spring-servlet.xml
./WEB-INF/web.xml
The application we are going to develop is a simple interaction with the server across a network (that is making a remote procedure call). The client will send a request to the server and the server will response to the client.A sevice is the server-side code that gets invoked from the client.
Our application can be run either in hosted-mode or we can be compiled into JavaScript and HTML to run it in the web-mode. If you use Eclipse IDE to develop your application simply generate an Eclipse project using the projectCreator script from your gwt installation directory:
projectCreator -eclipse SpringGwtIntegration
and then generate your GWT application using the applicationCreator shell:
applicationCreator -eclipse SpringGwtIntegration com.client.MyApplication
MyApplication.gwt.xml file is the GWT configuration file that specifies the location of the core web toolkit material and the entry point for the application.
./src/com/MyApplication.gwt.xml
Notice the defined servlet
In the public folder there is the HTML used primarily to test the application in the hosted mode:
./src/com/public/MyAplication.htm
In the “client” package we have the class that is our “main” application, and other two interfaces to the remote code. The MyService interface is implemented by the server-side code, in this case the MyServiceImpl class.
./src/com/client/MyService.java
package com.client;
import com.google.gwt.user.client.rpc.RemoteService;
public interface MyService extends RemoteService
{
public abstract String myMethod(String s);
}
The second one is used for the client-side code. The name of the interface MUST be the same as the server-side interface with "Async" appended. It must implement all of the methods in the server-side interface, but all of the methods MUST also take an additional parameter, which is an AsyncCallback object.
./src/com/client/MyServiceAsync.java
package com.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface MyServiceAsync
{
public abstract void myMethod(String s, AsyncCallback asynccallback);
}
The two "myMethod" methods differ by the additional AsyncCallback asynccallback parameter and the return type.
From the client application first you create an AsyncCallback object by using the GWT.create() method that takes the server-side interface as an argument and returns an object that uses the client-side interface.
We create an object instance that implements the AsyncCallback interface. Here we create an anonymous class for this. The server-side code is called asyncronously (the code won't wait for a response). That is why we need to create this object. This object will handle the result when it makes it's way back to the browser. We need to write code to handle both success and error conditions.
./src/com/client/MyApplication.java
package com.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.*;
// Referenced classes of package csw.client MyService, MyServiceAsync
public class MyApplication implements EntryPoint {
public MyApplication() {
}
public void onModuleLoad() {
final Label label = new Label();
final MyServiceAsync svc = (MyServiceAsync) GWT.create(com.client.MyService.class);
ServiceDefTarget endpoint = (ServiceDefTarget) svc;
endpoint.setServiceEntryPoint("services/myService");
final AsyncCallback callback = new AsyncCallback() {
public void onSuccess(Object result) {
label.setText(result.toString());
}
public void onFailure(Throwable ex) {
label.setText(ex.toString());
}
};
Button button = new Button("Click ME");
button.addClickListener(new ClickListener() {
public void onClick(Widget w) {
svc.myMethod("Do Something", callback);
}
});
RootPanel.get("testing").add(button);
RootPanel.get("testing2").add(label);
}
}
The server-side code:
./src/com/server/MyServiceImpl.java
package com.server;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.client.MyService;
public class MyServiceImpl extends RemoteServiceServlet
implements MyService
{
private static final long serialVersionUID = 1L;
public MyServiceImpl()
{
}
public String myMethod(String s)
{
return (new StringBuilder("You submitted: '")).append(s).append("'").toString();
}
}
To integrate this with Spring we only have to register our GWT servlet in our web.xml so we can simply use Spring's ServletForwardingController to call services by name.
We also have to modify a bit our GWT client to reflect a path change we have to do. We add to the web.xml file the Spring's DispatcherServlet mappings:
The web.xml will look like:
The last thing to do is to create a new configuration file called spring-servlet.xml where we put the real configuration of
our GWT's service controller.
The ServletWrappingController controls the RemoteServiceServlet's lifecycle completely, emulating the servlet-container.
package com.server;
import java.util.Enumeration;
import java.util.Properties;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
/**
* Spring Controller implementation that mimics standard ServletWrappingController behaviour
* (see its documentation), but with the important difference that it doesn't instantiate the
* Servlet instance directly but delegate for this the BeanContext, so that we can also use IoC.*
*/
public class ServletWrappingController extends AbstractController
implements BeanNameAware, InitializingBean, DisposableBean {
private Class servletClass;
private String servletName;
private Properties initParameters = new Properties();
private String beanName;
private Servlet servletInstance;
public void setServletClass(Class servletClass) {
System.out.print("setServletClass : "+servletClass);
this.servletClass = servletClass;
}
public void setServletName(String servletName) {
System.out.print("setServletName : "+servletName);
this.servletName = servletName;
}
public void setInitParameters(Properties initParameters) {
System.out.print("setInitParameters : "+initParameters);
this.initParameters = initParameters;
}
public void setBeanName(String name) {
System.out.print("setBeanName : "+name);
this.beanName = name;
}
public void setServletInstance(Servlet servletInstance) {
System.out.print("setServletInstance : "+servletInstance);
this.servletInstance = servletInstance;
}
public void afterPropertiesSet() throws Exception {
System.out.print("afterPropertiesSet");
if (this.servletInstance == null) {
throw new IllegalArgumentException("servletInstance is required");
}
if (!Servlet.class.isAssignableFrom(servletInstance.getClass())) {
throw new IllegalArgumentException("servletInstance [" + this.servletClass.getName() +
"] needs to implement interface [javax.servlet.Servlet]");
}
if (this.servletName == null) {
this.servletName = this.beanName;
}
this.servletInstance.init(new DelegatingServletConfig());
}
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
System.out.print("handleRequestInternal");
this.servletInstance.service(request, response);
return null;
}
public void destroy() {
System.out.print("destroy");
this.servletInstance.destroy();
}
private class DelegatingServletConfig implements ServletConfig {
public String getServletName() {
return servletName;
}
public ServletContext getServletContext() {
return getWebApplicationContext().getServletContext();
}
public String getInitParameter(String paramName) {
return initParameters.getProperty(paramName);
}
public Enumeration getInitParameterNames() {
return initParameters.keys();
}
}
}
Well, that was all folks. We only have to run the MyApplication-shell to run the application in the hosted-mode or compile the packages with the MyApplication-compile shell, add the generated output of the www directory to a SpringGwtIntegration war archive, add the necessary configuration files (web.xml, spring-servlet.xml, the bin and lib directories) and copy it in the server\default\deploy folder of your Jboss version.
Note: this application is deployed with the jboss-4.0.5.GA and jboss-3.2.8.SP1 versions of Jboss.
Any comment or suggestion is welcome.