Many users of Spring framework forget about or are not aware of important requirement which states that all HttpSession attributes must be serializable if the session should be passivable.
Let's start with some simple web application which has two session beans: UserBean and EditOrderBean and two singletons: UserDAO and OrderDAO. Those beans might look like those:
public class OrderDAO {
// actual code is not important in this context
}
public class UserDAO {
// actual code is not important in this context
}
public class UserBean {
private String login;
// userDAO is managed by Spring
private UserDAO userDAO;
// constructor, getters and setters
}
public class EditOrderBean {
private String item;
private int quantity;
// orderDAO is managed by Spring
private OrderDAO orderDAO;
// userBean is managed by Spring
private UserBean userBean;
// constructor, getters and setters
}
As simple as it seems it is not correct implementation and it will cause problems in distributed environment or on application server which has session passivation enabled. To fix this both UserBean and EditOrderBean bean classes must be serializable. To do so we should add Serializable interface to them and mark all non session bean references as transient.
public class UserBean implements Serializable {
private String login;
// userDAO is managed by Spring
private transient UserDAO userDAO;
// constructor, getters and setters
}
public class EditOrderBean implements Serializable {
private String item;
private int quantity;
// orderDAO is managed by Spring
private transient OrderDAO orderDAO;
// userBean is managed by Spring
private UserBean userBean;
// constructor, getters and setters
}
After those changes session will passivate but unfortunately after activation both our beans will be missing references to the beans which has been marked as transient. To reset them we must use little known method from Spring's AutowireCapableBeanFactory interface called configureBean. It gets two arguments: existing bean instance and beans name and performs bean initialization.
Call to this method should be made just after session activation but before its usage. This can be achieved using HttpSessionActivationListener interface.
public class RestoreSpringSessionListener implements
HttpSessionActivationListener {
@Override
public void sessionDidActivate(HttpSessionEvent sessionEvent) {
HttpSession session = sessionEvent.getSession();
ServletContext servletContext = session.getServletContext();
WebApplicationContext webAppContext = WebApplicationContextUtils.
getRequiredWebApplicationContext(servletContext);
AutowireCapableBeanFactory autowireFactory = webAppContext.
getAutowireCapableBeanFactory();
// Iterate through all session attributes, check if they are
// Spring managed beans and if they are reconfigure them.
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
// This checks only if the bean with given name is defined.
// It does not check if it has session scope.
if (webAppContext.containsBean(name)) {
Object bean = session.getAttribute(name);
// Reconfigure resored bean instance.
bean = autowireFactory.configureBean(bean, name);
session.setAttribute(name, bean);
}
}
}
@Override
public void sessionWillPassivate(HttpSessionEvent sessionEvent) {
}
}
Now you just need to register this listener in your web.xml:
<web-app ...>
...
<listener>
<listener-class>
com.acme.RestoreSpringSessionListener
</listener-class>
</listener>
...
</web-app>
Please be aware that this solution works only with quite simple bean configurations. This means that it won't work for instance if you are using:
- destroyable beans,
- complex bean initialization,
- proxies around beans,
- aop-scoped proxies.
I will try to address at least some of those limitations in the third part of this article.
Another solution for given problem is a custom implementation of session scope. This will be presented in second part of this article.