The first attempt actually was a transaction filter that always creates a transaction and then closes it, but this is expensive and one problem with GWT is that the SerializableException gets consumed and serialized as a response stream. So the filter will never see this exception being raised.
I have thus used Guice (pronounce this as "juice") to deal with these problems. The way it works is like this:
- A servlet context listener is used (see web.xml configuration) to initialize a Guice Injector and bind this to an attribute into the servlet context.
- A servlet filter always opens a Hibernate session. (not a transaction!). It will also (try-finally) close this session.
- When a GWT service needs to service a request, Tomcat will create the specified servlet.
- Each servlet derives from a BaseServiceImpl abstract servlet.
- The BaseServiceImpl overrides "init( ServletConfig config )". Through the "config.getServletContext().getAttribute()"
- I am retrieving the injector object, created in the listener.
- The injector object calls "injector.injectMembers( this )", which will "instrument" any annotated members in the servlet instance.
- When the injector sees a request to inject a field or method parameter, it will look this up in the registry and also attempt to inject any annotations that may exist in the to-be-injected instance.
- And so through very simple annotations, it may result in a couple of cascaded injection requests when a servlet gets instantiated.
Notice however that Guice is *not* installed on the classloader. This means that just setting "@Inject" on a field for example will not do anything *unless* you retrieve the instance through the injector instance. This latter part is not very easy for
everybody to understand right away, but is the most important aspect (no pun intended) about Guice programming as I have found so far.
Code example? You will need aopalliance.jar and guice-1.0.jar for this to run, downloadable from the Guice website:
ServletContextListener:
================
public class GuiceServletContextListener implements
ServletContextListener
{
public GuiceServletContextListener() {
super();
}
public void contextInitialized(ServletContextEvent servletContextEvent)
{
ServletContext servletContext =
servletContextEvent.getServletContext();
// Create our injector for our application use
// store it in servlet context.
Injector injector = Guice.createInjector( new TransactionModel() );
servletContext.setAttribute( Injector.class.getName(), injector );
}
public void contextDestroyed(
ServletContextEvent servletContextEvent)
{
}
}
TransactionModel:
=============
public class TransactionModel implements Module
{
public void configure(Binder binder)
{
binder.bindInterceptor(
any(), // Match classes.
annotatedWith(Transactional.class), // Match methods.
new TransactionInterceptor() // The interceptor.
);
}
}
Transactional:
==========
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Transactional {
}
BaseServiceImpl (a base class for any servlet in the application):
==========================================
public abstract class BaseServiceImpl extends RemoteServiceServlet {
.....
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
Injector injector = (Injector)config.getServletContext().
getAttribute( Injector.class.getName() );
injector.injectMembers( this );
}
.....
}
CustomerServiceImpl (implementation of GWT service):
====================================
public class CustomerServiceImpl extends BaseServiceImpl implements CustomerService {
......
@Inject
private CustomerBO customerBO;
......
public CustomerDTO saveCustomer( CustomerDTO dto, boolean isNew )
throws UIException
{
try {
Customer customer = customerBO.getCustomer( dto.getCustomerId() );
if ( customer == null ) {
checkAccess( WebConstants.CUSTOMER_ADD );
// not found, so create it.
customer = new Customer();
} else {
checkAccess( WebConstants.CUSTOMER_EDIT );
}
MapperIF mapper = DozerBeanMapperSingletonWrapper.getInstance();
mapper.map( custDTO, customer );
customer = customerBO.saveCustomer( customer, isNew );
CustomerDTO dto = customerBO.getCustomer( customer.getCustomerId() );
return dto;
} catch (ApplicationException ae ) {
log.error( "Could not save customer", ae );
throw new UIException( ae.getMessage() );
}
}
......
}
CustomerBO:
=========
@Singleton
public class CustomerBO extends BaseBO {
......
@Transactional
public Customer saveCustomer( Customer customer, boolean isNew )
throws ApplicationException
{
Session session = getSession();
try {
if ( isNew && customer != null ) {
throw new ApplicationException(
custI18n.getMessage( "error.cust.already.exists" ) );
}
// validate will raise an ApplicationException if the customer data is invalid.
validateCustomer( customer );
if ( isNew ) {
session.save( customer );
} else {
session.update( customer );
}
customer = getCustomer( customer.getCustomerId() );
return customer;
} catch (HibernateException he ) {
log.error( "Could not save customer", he );
throw new ApplicationException(
custI18n.getMessage( "error.save.customer" ));
}
}
}
==========================
Obviously this code can/should be extended with a variety of things. It should probably check if there is already a transaction ongoing. It should probably add parameters to the transaction interface to find out how the method supports transactions (required, supports, requiresNew, etc) and so on. But for simplicity's
sake, this is the bare minimum.
Notice how, once you have started the injector in the ServiceImpl, the CustomerBO does not need to be declared specifically in the injector. This is some sort of automatic cascading injection effect which happens because the injector is already processing dependent classes. So, luckily, you only need to use the Injector once, which will inject all your other classes where you want them.
Also have a look how to do this for interfaces. What is lacking in the CustomerBO is a separation of persistence with business logic. If you separate this further, you have a start to be able to switch the implementation of your persistence framework.
I am personally contemplating to bring the transaction boundary more forward and wrap this around the methods (where required) of ServiceImpl instead. But I am not sure whether this will work.
Good luck on your own incantations!
1 comment:
In my web applications I use multiple GWT modules and multiple remote service entry points... I also use Guice, and at some point I developed a small wrapper for RemoteServiceServlet that allows to use Guice managed beans as remote services for GWT.
Post a Comment