Update 30.12.2012: All examples are reviewed and updated to GWT 2.5. Enjoy!
Update 24.09.2011: I finally managed to push the complete tutorial project to github. It is based on GWT 2.4. 2.5. Looking forward to your feedback.
The first part of this tutorial describes how to set up a Request Factory (RF) based client-server communication in GWT. In contrast to the example at GWT home, a classic DAO pattern is used at the server-side with a clear separation between entity (passive, stateful) and dao/service (active, stateless). As this is not the default way, some additional helper classes (“locators”) need to be implemented. However, I think the benefit of a cleaner architecture is worth the price of some additional lines of code.
The second part will deal with testing RF-based classes in the GWT client.
Prerequisites
You should have the GWT SDK installed and a GWT compatible project in the IDE of your choice available. To make the example run you need some additional libraries on the classpath:
- org.json.*
- a JSR 303 Bean Validation implementation, e.g. hibernate-validator
When using Maven, simply add these dependencies:
<dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20090211</version> </dependency> <!-- Validation API --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.0.0.GA</version> </dependency> <!-- Validation Implementation --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.3.0.Final</version> </dependency> <!-- Need some logging provider for SLF4J --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>0.9.30</version> </dependency>
Without Maven you can use gwt-servlet-deps.jar from the GWT SDK (containing the JSON packages) and download the hibernate-validator jar with its dependencies.
Server-side model
First of all, let’s have a look at the server-side model which consists of two entities and a DAO.
package cleancodematters.server.domain; import java.util.List; public class Pizza { private Long id; private Long version; private String name; private List<Ingredient> ingredients; /* Getters and Setters */ }
package cleancodematters.server.domain; public class Ingredient { private String name; private boolean vegan; /* Getters and Setters */ }
package cleancodematters.server; import cleancodematters.server.domain.Pizza; public class PizzaDao { public void save( Pizza pizza ) { // save pizza instance } public Pizza findById( Long id ) { // look-up pizza instance return null; } }
In a real world scenario, the entities could be JPA-managed @Entitys, whereas the DAO could be a Spring @Repository with an EntityContext being injected.
Client-side model
In general it’s a good idea to not use back-end entities in the presentation layer as this breaks encapsulation. Normally, DTOs are created which are converted at the back-end’s external interface. With GWT RF this conversion is done automatically. All you need to do is to create an interface for each entity and define the needed getters and setters. All methods are mapped by naming conventions, i.e. their names must be identical.
There are two types of proxy entities so there are two base interfaces:
- EntityProxy is used for entities with an identity concept
- ValueProxy is used for all others. In fact, ValueProxy can be used to transfer any data between client and server.
Consequently, Pizza is an entity proxy whereas ingredient is a value proxy as ingredients are always attached to a pizza (in the true sense of the word).
package cleancodematters.client; import java.util.List; import cleancodematters.server.domain.Pizza; import com.google.web.bindery.requestfactory.shared.EntityProxy; import com.google.web.bindery.requestfactory.shared.ProxyFor; @ProxyFor(value = Pizza.class) public interface PizzaProxy extends EntityProxy { public Long getId(); public String getName(); public void setName( String name ); public List<IngredientProxy> getIngredients(); public void setIngredients( List<IngredientProxy> ingredients ); }
package cleancodematters.client; import cleancodematters.server.domain.Ingredient; import com.google.web.bindery.requestfactory.shared.ProxyFor; import com.google.web.bindery.requestfactory.shared.ValueProxy; @ProxyFor(value = Ingredient.class) public interface IngredientProxy extends ValueProxy { public String getName(); public void setName( String name ); public boolean isVegan(); public void setVegan( boolean vegan ); }
Two more things to mention:
- The static mapping of proxy interface to backend entity is done using the @ProxyFor annotation.
- When referencing associated types, the proxy interface is used (IngredientProxy instead of Ingredient)
Entity locator
By default, a couple of methods are expected to be existent in the backend entity for EntityProxies, e.g. findById. As we want to deal with pure pojos, a Locator class needs to be specified which is used by the RF for entity management. The locator can be specified within the @ProxyFor annotation as an optional argument:
import cleancodematters.server.PizzaLocator; @ProxyFor(value = Pizza.class, locator = PizzaLocator.class) public interface PizzaProxy extends EntityProxy { // ...
The implementation of is straight forward as method names provided by the Locator interface are self-explanatory.
package cleancodematters.server; import cleancodematters.server.domain.Pizza; import com.google.web.bindery.requestfactory.shared.Locator; public class PizzaLocator extends Locator<Pizza, Long>{ @Override public Pizza create( Class<? extends Pizza> clazz ) { return new Pizza(); } @Override public Pizza find( Class<? extends Pizza> clazz, Long id ) { return getPizzaDao().findById( id ); } private PizzaDao getPizzaDao() { return new PizzaDao(); } @Override public Class<Pizza> getDomainType() { return Pizza.class; } @Override public Long getId( Pizza domainObject ) { return domainObject.getId(); } @Override public Class<Long> getIdType() { return Long.class; } @Override public Object getVersion( Pizza domainObject ) { return domainObject.getVersion(); } }
Request Factory and Request Context
Now we have the back-end entity mapped and need some API in the GWT client that allows sending requests to the server. Again, we only define two interfaces, one extends RequestFactory and another extends RequestContext. The latter provides an abstraction of a single client-server request and consequently defines the methods that we want to call.
package cleancodematters.client; import cleancodematters.server.PizzaDao; import com.google.web.bindery.requestfactory.shared.Request; import com.google.web.bindery.requestfactory.shared.RequestContext; import com.google.web.bindery.requestfactory.shared.RequestFactory; import com.google.web.bindery.requestfactory.shared.Service; public interface PizzaRequestFactory extends RequestFactory { @Service(value = PizzaDao.class) public interface PizzaRequestContext extends RequestContext { Request<PizzaProxy> findById( Long id ); Request<Void> save( PizzaProxy pizza ); } PizzaRequestContext context(); }
Methods are mapped by naming convention, again. However, there are two differences:
- ProxyEntities (PizzaProxy) instead of back end entities (Pizza) are used.
- The return type for each method is wrapped within a Request class to represent the asynchronous nature of the communication.
The mapping between RequestContext and DAO is done in the @Service annotation. By default, all methods are expected to be static in the given server class. To change this, a ServiceLocator needs to be defined that performs a service lookup and returns a concrete DAO instance.
First, again the locator needs to be registered in the @Service annotation:
@Service(value = PizzaDao.class, locator = DaoLocator.class) public interface PizzaRequestContext extends RequestContext { //...
A very simple implementation may look like this:
package cleancodematters.server; import com.google.web.bindery.requestfactory.shared.ServiceLocator; public class DaoLocator implements ServiceLocator { @Override public Object getInstance( Class<?> clazz ) { // Create dao. Instance is cached by GWT so make sure the implementation is stateless. return new PizzaDao(); } }
RequestFactory and Spring
When using Spring, the ServiceLocator can perform a lookup in the current application context for the given class type. Here’s an example:
package cleancodematters.server; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import com.google.web.bindery.requestfactory.server.RequestFactoryServlet; import com.google.web.bindery.requestfactory.shared.ServiceLocator; public class WebApplicationContextServiceLocator implements ServiceLocator { @Override public Object getInstance( Class<?> clazz ) { HttpServletRequest request = RequestFactoryServlet.getThreadLocalRequest(); ServletContext servletContext = request.getSession().getServletContext(); ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext( servletContext ); return context.getBean( clazz ); } }
Besides the ServiceLocator, the EntityLocator can be implemented generically, too, to deal with any entity. You can use reflection to create instances and to retrieve a value for version and id.
After completing this tutorial, the client and server package should look similar to this screen:
Order a pizza
Finally, here’s a snippet that shows how to create a RequestFactory instance and to make a request to the server:
PizzaRequestFactory factory = GWT.create( PizzaRequestFactory.class ); factory.initialize( new SimpleEventBus() ); PizzaRequestContext context = factory.context(); PizzaProxy pizza = context.create( PizzaProxy.class ); pizza.setName( "Funghi" ); IngredientProxy cheese = context.create( IngredientProxy.class ); cheese.setName( "Cheese" ); cheese.setVegan( false ); pizza.setIngredients( Arrays.asList( cheese ) ); context.save( pizza ).fire();
You can pull the complete code embedded in an example project from github.