Problem
The problem is that when using MapSessionRepository the SessionRepositoryFilter will automatically sync the HttpSession to the Spring Session which overrides explicit use of the APIs. Specifically the following is happening:
- SessionRepositoryFilter is obtaining the current Spring Session. It caches it in the HttpServletRequest to ensure that every invocation of HttpServletRequest.getSession() does not make a database call. This cached version of the Spring Session has no attributes associated with it.
- The HomepageController obtains its own copy of Spring Session, modifies it, and then saves it.
- The JSP flushes the response which commits the HttpServletResponse. This means we must write out the session cookie just prior to the flush being set. We also need to ensure that the session is persisted at this point because immediately afterwards the client may have access to the session id and be able to make another request. This means that the Spring Session from #1 is saved with no attributes which overrides the session saved in #2.
- The IncludeController obtains the Spring Session that was saved from #3 (which has no attributes)
Solution
There are two options I see to solving this.
Use HttpSession APIs
So how would I solve this. The easiest approach is to stop using the Spring Session APIs directly. This is preferred anyways since we do not want to tie ourselves to the Spring Session APIs if possible. For example, instead of using the following:
@Controller
public class HomepageController {
@Resource(name = "sessionRepository")
private SessionRepository<ExpiringSession> sessionRepository;
@Resource(name = "sessionStrategy")
private HttpSessionStrategy sessionStrategy;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(final Model model) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final String sessionIds = sessionStrategy.getRequestedSessionId(request);
if (sessionIds != null) {
final ExpiringSession session = sessionRepository.getSession(sessionIds);
if (session != null) {
session.setAttribute("attr", "value");
sessionRepository.save(session);
model.addAttribute("session", session);
}
}
return "homepage";
}
}
@Controller
public class IncludeController {
private final static Logger LOG = LogManager.getLogger(IncludeController.class);
@Resource(name = "sessionRepository")
private SessionRepository<ExpiringSession> sessionRepository;
@Resource(name = "sessionStrategy")
private HttpSessionStrategy sessionStrategy;
@RequestMapping(value = "/include", method = RequestMethod.GET)
public String home(final Model model) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final String sessionIds = sessionStrategy.getRequestedSessionId(request);
if (sessionIds != null) {
final ExpiringSession session = sessionRepository.getSession(sessionIds);
if (session != null) {
LOG.error(session.getAttributeNames().size());
model.addAttribute("session", session);
}
}
return "include";
}
}
You can simplify it using the following:
@Controller
public class HomepageController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(HttpServletRequest request, Model model) {
String sessionIds = request.getRequestedSessionId();
if (sessionIds != null) {
final HttpSession session = request.getSession(false);
if (session != null) {
session.setAttribute("attr", "value");
model.addAttribute("session", session);
}
}
return "homepage";
}
}
@Controller
public class IncludeController {
@RequestMapping(value = "/include", method = RequestMethod.GET)
public String home(HttpServletRequest request, final Model model) {
final String sessionIds = request.getRequestedSessionId();
if (sessionIds != null) {
final HttpSession session = request.getSession(false);
if (session != null) {
model.addAttribute("session", session);
}
}
return "include";
}
}
Use RedisOperationsSessionRepository
Of course this may be problematic in the event that we cannot use the HttpSession API directly. To handle this, you need to use a different implementation of SessionRepository. For example, another fix is to use the RedisOperationsSessionRepository. This works because it is smart enough to only update attributes that have been changed.
This means in step #3 from above, the Redis implementation will only update the last accessed time since no other attributes were updated. When the IncludeController requests the Spring Session it will still see the attribute saved in HomepageController.
So why doesn't MapSessionRepository do this? Because MapSessionRepository is based on a Map which is an all or nothing thing. When the value is placed in the map it is a single put (we cannot break that up into multiple operations).