Saturday, June 30, 2007

More fun with Spring scopes / Java

Previous blog about custom scopes has shown an example of the thread-bound scopes (conversations that hold state per thread and live longer then request). This time I would like to show another interesting application for the custom bean scopes. In this case conversation is bound to the page in web application (or even to each unique set of request parameters for that page). Practical examples include per-page caching of the static data (i.e. for Ajax use), allow page visitors to interact or edit page content together and many others... Note that this post should be used as a starting point and may require some additional tweaking for more specific scenarios. Now let's look at the implementation.

First step is to capture the conversation identity. In general, several page urls may need to be mapped to the same scope identity, or even require special mapping from request url to the view. But for the sake of this illustration I will assume that each request url uniquely identifying the scope. Then I can use servlet Filter, which can be registered in web.xml and run before, Spring's own DispatcherServlet. This filter will store requestURI in thread local and provide static accessor method for retrieving that value.

public class PageScopeFilter implements Filter {
  private static ThreadLocal conversation = new ThreadLocal();
  
  public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    String conversationId = null;
    if (request instanceof HttpServletRequest) {
      conversationId = ((HttpServletRequest) request).getRequestURI();
    }
    conversation.set(conversationId);
    try {
      chain.doFilter(request, response);
    } finally {
      conversation.remove();
    }
  }

  public void init(FilterConfig config) throws ServletException {
  }

  public void destroy() {
  }
}  

Now, the PageScope implementation is rather trivial. Just maintain scope map of bean names to actual instances.

public class PageScope implements Scope {
  private final Map scope = new HashMap();
  
  private static final ObjectFactory MAP_FACTORY = new ObjectFactory() {
      public Object getObject() {
        return new HashMap();
      }
    };

  public String getConversationId() {
    return PageScopeFilter.getConversationId();
  }

  public synchronized Object get(String name, 
      ObjectFactory objectFactory) {
    Map beanMap = (Map) get(scope, getConversationId(), MAP_FACTORY);
    return get(beanMap, name, objectFactory);
  }

  public synchronized Object remove(String name) {
    Map beanMap = (Map) scope.get(getConversationId());
    return beanMap==null ? null : beanMap.remove(name);
  }
  
  public void registerDestructionCallback(String name, Runnable callback) {
    //
  }
  
  private Object get(Map map, String name, ObjectFactory factory) {
    Object bean = map.get(name);
    if(bean==null) {
      bean = factory.getObject();
      map.put(name, bean);
    }
    return bean;
  }
}

Note how ObjectFactory is used to construct a place holder for new conversation.

That is basically it. My first blog already shown how to register custom scopes, please refer there if you need more details, or feel free to ask question in comments for this post. Obvious enhancement of the above PageScope implementation could be to make it implement DisposableBean and hook with registered destruction callbacks, but I leave that exercise to the reader curiosity.

There is one more thing worth to mention. In case if page-bound data is mutable, in clustered environment, one may need to make it consistent across all cluster nodes. The one easy way to achieve that could be to use Terracotta for Spring, which has been recently open sourced.


Original article by Eugene Kuleshov

No comments: