問題
問題は、MapSessionRepository を使用すると、SessionRepositoryFilter が自動的に HttpSession を Spring Session に同期し、API の明示的な使用をオーバーライドすることです。具体的には、次のことが起こっています。
- SessionRepositoryFilter は現在の Spring Session を取得しています。これを HttpServletRequest にキャッシュして、HttpServletRequest.getSession() の呼び出しごとにデータベース呼び出しが行われないようにします。Spring Session のこのキャッシュされたバージョンには、それに関連付けられた属性はありません。
- HomepageController は、Spring Session の独自のコピーを取得し、それを変更してから保存します。
- JSP は、HttpServletResponse をコミットする応答をフラッシュします。これは、フラッシュが設定される直前にセッション Cookie を書き出す必要があることを意味します。また、この時点でセッションが永続化されていることを確認する必要があります。これは、直後にクライアントがセッション ID にアクセスして別のリクエストを作成できる可能性があるためです。これは、#1 からの Spring セッションが、#2 で保存されたセッションをオーバーライドする属性なしで保存されることを意味します。
- IncludeController は、#3 から保存された Spring Session を取得します (属性はありません)。
解決
これを解決するには2つのオプションがあります。
HttpSession API を使用する
では、どうすればこれを解決できますか。最も簡単な方法は、Spring Session API を直接使用するのをやめることです。可能であればSpring Session APIに縛られたくないので、とにかくこれが好ましいです。たとえば、次を使用する代わりに:
@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";
}
}
以下を使用して単純化できます。
@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";
}
}
RedisOperationsSessionRepository を使用する
もちろん、HttpSession API を直接使用できない場合、これは問題になる可能性があります。これを処理するには、SessionRepository の別の実装を使用する必要があります。たとえば、別の修正は RedisOperationsSessionRepository を使用することです。これは、変更された属性のみを更新するのに十分スマートであるため機能します。
これは、上記のステップ 3 で、他の属性が更新されていないため、Redis 実装は最終アクセス時刻のみを更新することを意味します。IncludeController が Spring Session を要求すると、HomepageController に保存された属性が引き続き表示されます。
では、なぜ MapSessionRepository はこれを行わないのでしょうか? MapSessionRepository は、全か無かのマップに基づいているためです。値がマップに配置されると、それは単一のプットです (複数の操作に分割することはできません)。