Add conversation support to Spring MVC

133/365  Cupid

Creative Commons License photo credit: martinak15

One thing I miss in Spring-MVC is the concept of a conversation-scoped attribute. It was supposed to be added in v3.2, but seems to have been pushed back – and I think it might not be done at all, as Spring Webflow currently supports it. However – using webflow just for the sake of enabling a conversation scope seems a bit harsh for my humble Spring-MVC applications. So I went in search of a solution, but ended up writing one myself.

The problem

Suppose you have a CRUD controller. Create, Read and Delete are trivial, but there is always an issue with the Update action, and it seems like there’s just not a globally accepted solution. Many blog entries (and books too!) skip the issue altogether, and the two main options that seem prevalent with Spring-MVC are either:

1. Use a DTO or a form object.

When you do that, your application normally follows this recipe:

  1. On GET – read the domain object from the database
  2. Create a DTO or a Form object, and populate it using the data in the domain-object. (Not all attributes of the model must make their way into the DTO)
  3. Make sure to be able to retrieve the underlying data later (usually by adding an object identifier to the form as a hidden field, or by using the id in the URL)
  4. Display the form to the user.
  5. On PUT/POST – re-read the underlying domain object
  6. Copy or merge the attributes from the DTO into the domain object
  7. save the domain object

This is a lot of work – but it works.

It gets complicated with holding on to the identifier used to retrieve the object originally – as you may expose yourself to some issues if the identifier is exposed to the user (who may change the id of a customer and save it?) – but there are well-understood methods around that.

It also gets a bit more complicated if you use version in your models – because now you need to retain both the identifier and the version. In short – it may get a bit messy.

2. Use @SessionAttributes

The @SessionAttributes annotation allows you to store objects between requests. For example – consider that you have the following Controller to edit a customer’s details:

@Controller
@SessionAttributes("customer")
public class CustomerEditController {

  //wiring ommitted

  @RequestMapping(value = "/customer/{code}/edit", method = RequestMethod.GET)
  public String get(Model model, @PathVariable("code") String code) {
    Customer customer = customerService.findByCustomerCode(code);
    model.addAttribute(customer);
    return "/customer/edit";
  }

  @RequestMapping(value = "/customer/{code}/edit", method = RequestMethod.PUT)
  public String get(@PathVariable("code") String code,@ModelAttribute("customer") Customer customer, BindingResult result ) {

    if (result.hasErrors()) {
      //deal with errors, redirect somewhere
    }    
    customerService.save(code); //This will just delegate to the repository save() method. No magic - just an illusion.
    //Add success message perhaps?
    return "redirect:/customer/{code}/edit";
  }

}

@SessionAttributes as defined above – will pick up adding the customer to the model, and will store it on the session (using the DefaultSessionAttributeStore class). Since this is the command that we bind in the form:form tag – we are in essence using the model as a DTO – and saving a lot of coding on the way.
This is why, when we return on the PUT/POST request, the customer object is there, with its database id, version and everything. And this is why the save method works – because we actually have the object with everything we need on it.

So – this seems to work perfectly, no?

Sadly, no.
Let’s say I open two browser tabs – one to edit customer “APPL” and the other to edit customer “IBM” (so – if we carry on with our request mapping above: http://example.com/customer/APPL/edit and http://example.com/customer/IBM/edit). You’d expect this to work, but when you try it – you’ll see that the last save wins, even if these are two different customers. The reason is that there’s only one instance of the customer object stored inside your session.
This has to do with the implementation of the DefaultSessionAttributeStore in Spring:

//class: org.springframework.web.bind.support.DefaultSessionAttributeStore

public void storeAttribute(WebRequest request, String attributeName, Object attributeValue) {
  Assert.notNull(request, "WebRequest must not be null");
  Assert.notNull(attributeName, "Attribute name must not be null");
  Assert.notNull(attributeValue, "Attribute value must not be null");
  String storeAttributeName = getAttributeNameInSession(request, attributeName);
  request.setAttribute(storeAttributeName, attributeValue, WebRequest.SCOPE_SESSION);
}

protected String getAttributeNameInSession(WebRequest request, String attributeName) {
  return this.attributeNamePrefix + attributeName;
}

The attributeNamePrefix may be changed – but it does help much. With every call to storeAttribute = the DefaultSessionAttributeStore just stores the attribute under its name on the request. If the request already had an attribute with this name stored – tough luck. You think you are updating IBM, but you actually update Apple. !Happy.

Clearing attributes from the session store

As if that wasn’t enough pain, we still have the issue of clearing those objects from the session. We don’t want stuff hanging around in users’ sessions, because it takes up resources. Therefore we need to remove attributes we stored when we no longer need them. But – Spring can’t know when to remove attributes by itself, because it can’t guess the way the user navigates through your system. It is therefore completely up to you to clear the session of residual data, using SessionStatus.setComplete():

//CustomerEditController with a tiny update
@RequestMapping(value = "/customer/{code}/edit", method = RequestMethod.PUT)
  public String get(@PathVariable("code") String code,@ModelAttribute("customer") Customer customer, BindingResult result,SessionStatus sessionStatus) {

    if (result.hasErrors()) {
      //deal with errors, redirect somewhere
    }    
    customerService.save(code); //This will just delegate to the repository save() method. No magic - just an illusion.
    <strong>sessionStatus.setComplete();</strong>   
    //Add success message perhaps?
    return "redirect:/customer/{code}/edit";
  }

By marking the SessionStatus as complete, we say that the attributes stored using our SessionAttributeStore implementation (in the default case this is the DefaultSessionAttributeStore) are ripe for removal. And indeed, the org.springframework.web.bind.annotation.support.HandlerMethodInvoker will do this for us gladly – but only when the session status is indeed marked as complete. Otherwise – the objects will stay there forever, or until the session is deleted, or until a new object of the same name is stored on the session. Such is life.

Now – it is probably that you don’t know better than Spring when to call setComplete(). What if a user opens the edit page, causing your customer model to be set on the session, but then navigates away to a different page and never posts the customer data back to the controller? In this case – your SessionAttributeStore can get filled up quite quickly with orphaned objects. This may result in tears down the road.

Mission Statement

The mission then is to try and solve both these problems – create a mechanism to store multiple objects of the same type on the session using the @SessionAttributes, annotation, and clear up the stale data from the session attribute store even if the code did not pass through a setComplete() method call.

To do this, you can create a custom implementation of the SessionAttributeStore (which is an extension point in Spring).
I started with this wonderful post. The code (that you can download from there) adds conversation support to the SessionAttributeStore – but it uses a taglib to add the conversation id to the view, and I am too lazy to do that on every form – so I changed it a bit.
(The full code is on git)

public class ConversationalSessionAttributeStore implements SessionAttributeStore, InitializingBean {

  @Inject
  private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
  private Logger logger = Logger.getLogger(ConversationalSessionAttributeStore.class.getName());

  private int keepAliveConversations = 10;

  public final static String CID_FIELD = "_cid";
  public final static String SESSION_MAP = "sessionConversationMap";

  @Override
  public void storeAttribute(WebRequest request, String attributeName, Object attributeValue) {
    Assert.notNull(request, "WebRequest must not be null");
    Assert.notNull(attributeName, "Attribute name must not be null");
    Assert.notNull(attributeValue, "Attribute value must not be null");

    String cId = getConversationId(request);
    if (cId == null || cId.trim().length() == 0) {
      cId = UUID.randomUUID().toString();
      request.setAttribute(CID_FIELD, cId, WebRequest.SCOPE_REQUEST);
    }

    logger.debug("storeAttribute - storing bean reference for (" + attributeName + ").");
    store(request, attributeName, attributeValue, cId);
  }

  private String getConversationId(WebRequest request) {
    return request.getParameter(CID_FIELD);
  }
  1. The storeAttribute method gets called by the framework when @SessionAttributes needs to store an attribute.
  2. Line 7: declares an attribute which is the number of conversations to keep alive at any one point (consider this the number of open tabs). It is configurable, as we’ll see later, and is important in the cleanup process.
  3. Lines 18-22 – we get the conversation id from the request if it exists (it will only exist on PUT/POST, because we are using request.getParameter() inside the getConversationId() method. This in effect creates a new conversation with every get request – which is the desired behaviour (otherwise – Spring Webflow is probably the answer)
  4. Line 25 – Call the store method to actually store the new attribute on the request (see next piece of code below)
private void store(WebRequest request, String attributeName, Object attributeValue, String cId) {
  LinkedHashMap<String, Map<String, Object>> sessionConversationsMap = getSessionConversationsMap(request);
  if (keepAliveConversations > 0 && sessionConversationsMap.size() >= keepAliveConversations
    && !sessionConversationsMap.containsKey(cId)) {

    // clear oldest conversation
    String key = sessionConversationsMap.keySet().iterator().next();
    sessionConversationsMap.remove(key);
  }
  getConversationStore(request, cId).put(attributeName, attributeValue);
}

private LinkedHashMap<String, Map<String, Object>> getSessionConversationsMap(WebRequest request) {
  @SuppressWarnings("unchecked")
  LinkedHashMap<String, Map<String, Object>> sessionMap = (LinkedHashMap<String, Map<String, Object>>) request.getAttribute(
    SESSION_MAP, WebRequest.SCOPE_SESSION);
  
  if (sessionMap == null) {
    sessionMap = new LinkedHashMap<String, Map<String, Object>>();
    request.setAttribute(SESSION_MAP, sessionMap, WebRequest.SCOPE_SESSION);
  }

  return sessionMap;
}

private Map<String, Object> getConversationStore(WebRequest request, String conversationId) {
  Map<String, Object> conversationMap = getSessionConversationsMap(request).get(conversationId);
  if (conversationId != null && conversationMap == null) {
    conversationMap = new HashMap<String, Object>();
    getSessionConversationsMap(request).put(conversationId, conversationMap);
  }
  return conversationMap;
}
  1. The way the conversational session store is implemented is by holding a map of maps on the session. The main-map is the sessionsConversationsMap, with the conversation id as a key, and the conversation-specific @SessionAttributes as the value (a Map<String,Object>). In line 2, the store method calls on the getSessionConversationsMap() method to get a handle to this map.
  2. The getSessionConversationsMap() method in lines 13-24 retrieves the conversation map from the session, and creates it if it does not exist. Note that the map is stored in a Session scope, and that the code specifically uses the LinkedHashMap implementation. This is important for cleaning up later.
  3. Lines 3-9 deal with cleanup support. If the map contains more than keepAliveConversations conversations, and does not contain the current conversation id – then the oldest conversation must be removed. This is where the LinkedHashMap comes into play, because its iterator.next() call guarantees a FIFO traversal of the list, effectively removing the oldest conversation’s map from our sessionConversationsMap.
  4. The getConversationStore method in lines 26-33 deals with retrieving or creating a conversation store for the conversation id in the request.
  5. At line 10: the store method stores the attribute it received into the conversation’s map. After this is done, the object(s) we wanted to store using @SessionAttributes are stored as a key-value map under the conversation id on the session.
@Override
  public Object retrieveAttribute(WebRequest request, String attributeName) {
    Assert.notNull(request, "WebRequest must not be null");
    Assert.notNull(attributeName, "Attribute name must not be null");

    if (getConversationId(request) != null) {
      if (logger.isDebugEnabled()) {
        logger.debug("retrieveAttribute - retrieving bean reference for (" + attributeName + ") for conversation ("
          + getConversationId(request) + ").");
      }
      return getConversationStore(request, getConversationId(request)).get(attributeName);
    } else {
      return null;
    }
}

@Override
public void cleanupAttribute(WebRequest request, String attributeName) {
  Assert.notNull(request, "WebRequest must not be null");
  Assert.notNull(attributeName, "Attribute name must not be null");

  if (logger.isDebugEnabled()) {
    logger.debug("cleanupAttribute - removing bean reference for (" + attributeName + ") from conversation ("
      + getConversationId(request) + ").");
  }

  Map<String, Object> conversationStore = getConversationStore(request, getConversationId(request));
  conversationStore.remove(attributeName);

  // Delete the conversation store from the session if empty
  if (conversationStore.isEmpty()) {
    getSessionConversationsMap(request).remove(getConversationId(request));
  }
}
  1. The implemented retrieveAttribute code is quite simple. If the request has a conversation id – the method looks at the conversation store for the specific conversation, and returns the value mapped to the attributeName key.
  2. The implemented cleanupAttribute method will be called by the framework when the SessionStatus.setComplete() was called (as explained above).

This is mostly “it” for the ConversationalSessionAttributeStore. But how can you use it?

Tie it all together

To use the new ConversationalSessionAttributeStore in your application, you’ll have to do the following (Assuming Spring version 3.1 or later):

  1. Implement InitializingBean in the ConversationalSessionAttributeStore. This means that you’ll have to implement the afterPropertiesSet() as well:
    @Override
    public void afterPropertiesSet() throws Exception {
      requestMappingHandlerAdapter.setSessionAttributeStore(this);
    }
    

    Doing this will replace the DefaultSessionAttributeStore with our implementation.

  2. Configure this bean to Spring:
    <bean id="conversationalSessionAttributeStore" class="com.duckranger.conversationsupport.ConversationalSessionAttributeStore">
      <property name="keepAliveConversations" value="10"/>
    </bean>
    

    (Note the configuration for keepAliveConversations discussed before. Also note that if you set it to 0 – conversations will not be deleted from the session).

Add conversation id to forms

Replacing the default session attribute store is good – but it relies on adding the conversation ids inside forms (remember – we need the conversation id as a request.getParameter()). To do this, I use a custom org.springframework.web.servlet.support.RequestDataValueProcessor:

package com.duckranger.conversationsupport;

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.support.RequestDataValueProcessor;

public class ConversationIdRequestProcessor implements RequestDataValueProcessor {

  @Override
  public String processAction(HttpServletRequest request, String action) {
    return action;
  }

  @Override
  public String processFormFieldValue(HttpServletRequest request, String name, String value, String type) {
    return value;
  }

  @Override
  public Map<String, String> getExtraHiddenFields(HttpServletRequest request) {
    Map<String, String> hiddenFields = new HashMap<String, String>();
    if (request.getAttribute(ConversationalSessionAttributeStore.CID_FIELD) != null) {
      hiddenFields.put(ConversationalSessionAttributeStore.CID_FIELD,
        request.getAttribute(ConversationalSessionAttributeStore.CID_FIELD).toString());
    }
    return hiddenFields;
  }

  @Override
  public String processUrl(HttpServletRequest request, String url) {
    return url;
  }
}

The RequestDataValueProcessor lets you interfere with forms. In my implementation, I use the getExtraHiddenFields() method to add new hidden fields into a form.
All I need to do is check whether the request contains a conversation-id attribute (remember that in ConversationalSessionAttributeStore.storeAttribute() we set the new conversation id on a request – this only does so to GET requests, so when the form needs to be rendered, this attribute is present).

If the conversation id attribute is present, I add it as a hidden field on the form, which will be rendered like so:

<input type="hidden" value="311aecd6-c629-415c-be35-673b3406eb0f" name="_cid">

And, on our way back (via Post or PUT) – this field is submitted, together with the form, as a request parameter.

To configure this RequestDataValueProcessor – you’ll need to add the following bean:

<bean name="requestDataValueProcessor" 
  class="com.duckranger.conversationsupport.ConversationIdRequestProcessor"/>

And that is all (unless I missed something…) – head over to the git repository for the complete code.

33 thoughts on “Add conversation support to Spring MVC

  1. Eduard

    Great post. I’m about to give it a try! …As of 2013-03-13, some code listings use a variable named “customerCode”, but there is no such variable declared; the following variables are declared: “customer” and “code”. I guess “customerCode” should be named “code” – not that this modification matters for OP’s end result, but the modification could help follow OP’s chain of reasoning.

  2. cptnimo Post author

    @Eduard – you are right – I have changed the code in the listing now..

  3. Pingback: Conversation Scope | UREILESS

  4. Jose Juan Montiel

    Hi, i’m going to try this solution in “complex” spring mvc app… i “redevelop” a simple solution java web app, here… https://github.com/josejuanmontiel/subsession but in complex spring mvc… miss some points…

    My question is: what about rest request? Could be possible modify the “solution” to use “cookies”?

    what do you think?

  5. Duck Ranger Post author

    @jose – not sure what you are trying to achieve. The cookies you are adding with the jquery plugin shouldn’t be used for a web application – as they won’t be httponly. Therefore they are client-side only cookies. I can see a use for them if you’re using it with an ajax request – and then it doesn’t matter whether you have a conversation scope or not.
    My idea with the Spring conversation scope support is more to allow the server to decipher which form it created was submitted. For restful requests in your case – I don’t think you need to change my solution much

  6. Tirumal Reddy

    Hi Duck,

    Thanks for the post. Its very interesting. I am into the same scenario and wants to implement this solution in my application. I dont know whether i am missing something or not? it didn’t worked for me. when i try to view the source i was unable to find the hidden field.

    Is there any other configuration which is mandatory for this fix?

  7. José

    Hey, great solution dude, thanks 4 post.

    I’m try to make this for @ResponseBody (JSON) but it can be difficult. Please please give me a rope which climb. Maybe inject a parameter on JSON or a Header.

    Thanks

  8. FallenAngel321

    Hello, I hope people are still reading this,
    When implementing this approach in Spring 3.2, I get a null pointer exception when starting the web container in the afterPropertiesSet() method. I am assuming this is because the requestMappingHandlerAdapter is null, but I am new to spring and not sure why the inject statement did not work. Anyone have any thoughts?

  9. Duck Ranger Post author

    @FallenAngel – I haven’t checked this in Spring 3.2 Will try and post back

  10. FallenAngel321

    My bad guys, I had the configuration messed up between the root and servlet context files. I have this running and am looking forward very much to working with it. Thank you for this post, it’s pretty much the only one I’ve found that details this process. For someone new to spring and looking for this type of functionality, you’re a life saver.

  11. GG

    Hi @Duck Ranger

    Has anyone made this solution to work? I have been trying to make this work but it is not creating the c_id and it is not picking up when I try to step up on the code. I see that you have put the setComplete at Get method but the Marty example has it on the Post method ?

    Do you have an example when you used it on the project ?

  12. Duck Ranger Post author

    @GG – what version of Spring are you using? I am using it in production with 3.2 and it works fine.
    The get method you see is actually a PUT method – the method name is misleading, I will change it :)

    You must remember to define the requestDataValueProcessor bean, otherwise the c_id generator will never be picked up by the framework.

  13. GG

    @Duck Ranger, I am using Spring 3.1 and I made it to work, but now my problem is that I dont need the solution to be used by all my controller but just few of them, and the problem is the Session attribute store and the requestData processor are living in dispatcher xml file, I know this is out of the scope but is there a way to exclude these two from controller that I dont need use it ?

  14. Ben

    I wanted to give a heads up that with Spring 4 the RequestDataValueProcessor interface has changed. Also, I was curious for thoughts on how to combine this solution with the standard Spring Security solution for Cross Site Request Forgery which is also based on using a RequestDataValueProcessor: here. My initial thought is to instantiate a local copy of Spring Security’s class as a delegate that each of your methods calls down into before adding it’s own logic. My initial thought was to just reference the _csrf token in the Session Attribute Store, but now I am leaning towards keeping them separate. What are your thoughts?

  15. mandeep

    @GG what change did you had to make to get the cid getting generated. For some reason mine is not picking up ConversationalSessionAttributeStore.

  16. Andrzej

    It is useless, because on F5 (reload the same window) it creates new CID. This solution does not distinguish new tab and the same page refresh.

  17. niim

    @Andrzej i think you missed the point. this is the solution to distinguish user’s each model data for each request stored in session. request by same page refresh or not is not the point.

  18. sprinMvc

    This does not work – null pointer exception!
    Error creating bean with name ‘conversationalSessionAttributeStore’ defined in class path resource [context/application-context.xml]: Invocation of init method failed; nested exception is java.lang.NullPointerException:

  19. chandra

    Thanks Ranger for response
    I am using spring 3.2.1 version but not working please help me on this. I

    The code I am using as for below.

    HelloWeb-servlet.xml
    =====================

    The error I am getting when I try to access my application

    =======================================================
    [ERROR ] Context initialization failed
    Error creating bean with name ‘conversationalSessionAttributeStore’: Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter com.duckranger.conversationsupport.ConversationalSessionAttributeStore.requestMappingHandlerAdapter; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter] is defined: expected single matching bean but found 2: org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#0,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#1
    [ERROR ] Uncaught.init.exception.thrown.by.servlet
    HelloWeb
    CrunchifySpringMVC3.2.1
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘conversationalSessionAttributeStore’: Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter com.duckranger.conversationsupport.ConversationalSessionAttributeStore.requestMappingHandlerAdapter; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type

    if I remove I am getting different error
    =============================================================

    ERROR ] SRVE0777E: Exception thrown by application class ‘org.springframework.web.servlet.DispatcherServlet.getHandlerAdapter:1,128′
    javax.servlet.ServletException: No adapter for handler [com.tutorialspoint.StudentController@6381dc57]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler
    at org.springframework.web.servlet.DispatcherServlet.getHandlerAdapter(DispatcherServlet.java:1128)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:903)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:920)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:816)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:575)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:801)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:668)
    at com.ibm.ws.webcontainer.servlet.ServletWrapper.service(ServletWrapper.java:1274)
    at [internal classes]

  20. chandra

    If possible please upload your sample application which is working for you that would help me lot

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>