Spring-session 在 pageContext.include 之后从会话中清除属性
Spring-session clears attributes from session after pageContext.include
我想我在spring-session
中遇到了一个错误,但我只想在这里问一下它是否真的是一个错误。在我忘记之前
https://github.com/paranoiabla/spring-session-issue.git
这里是重现该问题的 github 存储库。基本上我有 2 个控制器和 2 个 jsps,所以流程是这样的:
- 用户打开
http://localhost:8080/
并且流程经过 HomepageController
,这将 1 个属性放入 spring-session 和 returns 呈现的 homepage.jsp
会话 ID 和属性数 (1)
homepage.jsp
里面有这一行:
${pageContext.include("/include")}
调用要调用的 IncludeController
。
-
IncludeController
从会话存储库中找到会话并记录属性的数量(现在绝对奇怪它们被记录为 0)和 returns include.jsp
呈现两者会话 ID 和会话属性的数量 (0)。
两个 jsps 中的会话 ID 是相同的,但是在 pageContext.include
调用之后不知何故属性被重置为空映射!!!
有人可以确认这是否是错误。
谢谢。
问题
问题是,当使用 MapSessionRepository 时,SessionRepositoryFilter 会自动将 HttpSession 同步到 Spring Session,这会覆盖 APIs 的显式使用。具体发生了以下情况:
- SessionRepositoryFilter 正在获取当前 Spring 会话。它将它缓存在 HttpServletRequest 中以确保 HttpServletRequest.getSession() 的每次调用都不会进行数据库调用。此 Spring 会话的缓存版本没有与之关联的属性。
- HomepageController 获取自己的 Spring Session 副本,对其进行修改,然后保存。
- JSP 刷新提交 HttpServletResponse 的响应。这意味着我们必须在设置刷新之前写出会话 cookie。我们还需要确保会话在此时保持不变,因为此后客户端可能会立即访问会话 ID 并能够发出另一个请求。这意味着来自 #1 的 Spring 会话保存时没有覆盖 #2 中保存的会话的属性。
- IncludeController 获取从#3 保存的 Spring 会话(没有属性)
解决方案
我认为有两个选项可以解决这个问题。
使用 HttpSession APIs
那么我该如何解决这个问题。最简单的方法是直接停止使用 Spring Session API。无论如何,这是首选,因为如果可能的话,我们不想将自己绑定到 Spring 会话 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 会话时,它仍然会看到保存在 HomepageController 中的属性。
那么为什么 MapSessionRepository 不这样做呢?因为 MapSessionRepository 是基于 Map 的,它是一个全有或全无的东西。当值被放置在映射中时,它是一个单独的放置(我们不能将其分解为多个操作)。
我想我在spring-session
中遇到了一个错误,但我只想在这里问一下它是否真的是一个错误。在我忘记之前
https://github.com/paranoiabla/spring-session-issue.git
这里是重现该问题的 github 存储库。基本上我有 2 个控制器和 2 个 jsps,所以流程是这样的:
- 用户打开
http://localhost:8080/
并且流程经过HomepageController
,这将 1 个属性放入 spring-session 和 returns 呈现的homepage.jsp
会话 ID 和属性数 (1) homepage.jsp
里面有这一行:${pageContext.include("/include")}
调用要调用的IncludeController
。-
IncludeController
从会话存储库中找到会话并记录属性的数量(现在绝对奇怪它们被记录为 0)和 returnsinclude.jsp
呈现两者会话 ID 和会话属性的数量 (0)。 两个 jsps 中的会话 ID 是相同的,但是在pageContext.include
调用之后不知何故属性被重置为空映射!!! 有人可以确认这是否是错误。
谢谢。
问题
问题是,当使用 MapSessionRepository 时,SessionRepositoryFilter 会自动将 HttpSession 同步到 Spring Session,这会覆盖 APIs 的显式使用。具体发生了以下情况:
- SessionRepositoryFilter 正在获取当前 Spring 会话。它将它缓存在 HttpServletRequest 中以确保 HttpServletRequest.getSession() 的每次调用都不会进行数据库调用。此 Spring 会话的缓存版本没有与之关联的属性。
- HomepageController 获取自己的 Spring Session 副本,对其进行修改,然后保存。
- JSP 刷新提交 HttpServletResponse 的响应。这意味着我们必须在设置刷新之前写出会话 cookie。我们还需要确保会话在此时保持不变,因为此后客户端可能会立即访问会话 ID 并能够发出另一个请求。这意味着来自 #1 的 Spring 会话保存时没有覆盖 #2 中保存的会话的属性。
- IncludeController 获取从#3 保存的 Spring 会话(没有属性)
解决方案
我认为有两个选项可以解决这个问题。
使用 HttpSession APIs
那么我该如何解决这个问题。最简单的方法是直接停止使用 Spring Session API。无论如何,这是首选,因为如果可能的话,我们不想将自己绑定到 Spring 会话 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 会话时,它仍然会看到保存在 HomepageController 中的属性。
那么为什么 MapSessionRepository 不这样做呢?因为 MapSessionRepository 是基于 Map 的,它是一个全有或全无的东西。当值被放置在映射中时,它是一个单独的放置(我们不能将其分解为多个操作)。