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:
- On GET – read the domain object from the database
- 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)
- 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)
- Display the form to the user.
- On PUT/POST – re-read the underlying domain object
- Copy or merge the attributes from the DTO into the domain object
- 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);
}
- The storeAttribute method gets called by the framework when @SessionAttributes needs to store an attribute.
- 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.
- 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)
- 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;
}
- 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.
- 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.
- 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.
- The getConversationStore method in lines 26-33 deals with retrieving or creating a conversation store for the conversation id in the request.
- 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));
}
}
- 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.
- 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):
- 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.
- 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.

thanX dear.very very usefull…..
ganesh is it working for you???
@daniele – is it not working for you?
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.
@Eduard – you are right – I have changed the code in the listing now..
Pingback: Conversation Scope | UREILESS