Spring-Session源码研究之processRequest
来源:互联网 发布:wap页面纯文字游戏源码 编辑:程序博客网 时间:2024/05/21 08:48
之前的两章里面我们分别讨论了Spring-Session的配置这一块. 讨论其是如何将自己并入到Servlet生命周期中的, 以及在并入时如何准备自身正常工作所需要的条件.
而本章节讲解Spring-Session在一次完整的请求过程中是如何处理Session相关的问题.
所以我们的关注重点是
SessionRepositoryFilter<S extends ExpiringSession>
1. 概述
由上一篇文章我们已经了解到了, SessionRepositoryFilter
就是作为处理请求的第一个Filter, 在其内部实现中会将tomcat等servlet容器对ServletRequest
和ServletResponse
的实现使用Wrapper模式, 以加入自定义的逻辑——共享session.
2. OncePerRequestFilter
类
SessionRepositoryFilter
扩展自OncePerRequestFilter
.OncePerRequestFilter
类看名称就能猜到本类封装的是保证本Filter在一次完整的拦截链中只执行一次的逻辑, 因为spring无法保证同一个Filter
类只有一个实例。有可能一个Filter
既有可能在web.xml里配置由容器初始化了,还有可能被作为spring的依赖引入进了DelegatingFilterProxy
。这样在一次filter chain中就会存在同一个Filter
的多个实例, 所以OncePerRequestFilter
类通过向request参数中插入一个Boolean
类型的标识来确保本类Filter只执行一次(不论本拦截链有多个本类型的实例).- ,
OncePerRequestFilter
类对接口Filter
的实现采用了final
, 而将扩展途径以doFilterInternal
开放给子类.
3. doFilterInternal
方法
接下来我们就来看看SessionRepositoryFilter
对doFilterInternal
的实现
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // SESSION_REPOSITORY_ATTR = SessionRepository.class.getName(); // 将sessionRepository推入request, 留待后续使用. request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); // Wrapper模式 SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper( wrappedRequest, response); // 策略模式, 给外界一个机会介入, 加入自定义逻辑. // 默认实现是CookieHttpSessionStrategy, 即使用cookie进行标识用户 // CookieHttpSessionStrategy的wrapRequest实现是将CookieHttpSessionStrategy实例自身作为Attribute插入到request中. HttpServletRequest strategyRequest = this.httpSessionStrategy .wrapRequest(wrappedRequest, wrappedResponse); // 略 HttpServletResponse strategyResponse = this.httpSessionStrategy .wrapResponse(wrappedRequest, wrappedResponse); try { // 使用经过两次封装处理之后的request和response替换掉之前的request和response // 并推动链条继续向下执行 filterChain.doFilter(strategyRequest, strategyResponse); } finally { // 确保提交, 这种位置一共两处, 还有一处一会再揭晓. wrappedRequest.commitSession(); }}
4. SessionRepositoryRequestWrapper
类
这也是我们本次关注的重点. 其中最关键的当然就是getSession
方法了, 毕竟这才是我们在使用Session时最常用的方法.
@Overridepublic HttpSessionWrapper getSession(boolean create) { // getCurrentSession()方法的实现如下: // (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR); // 如果本次request过程中已经提取过session, 则直接返回. HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } // 这里的默认实现是由CookieHttpSessionStrategy类完成的 // 获取本次请求里的SessionId, // 因为Spring-Session是支持同一个浏览器多用户的, 所以我们需要依据约定来提取当前用户对应的那个SessionId(约定的细节留待之后再进行详述). String requestedSessionId = getRequestedSessionId(); if (requestedSessionId != null && getAttribute(INVALID_SESSION_ID_ATTR) == null) { // 使用指定的requestedSessionId从外部存储(例如Redis)中获取Session // 该方法的默认实现是由`RedisOperationsSessionRepository`类完成. // 从Redis中提取存储的信息, 组装成RedisSession实例. S session = getSession(requestedSessionId); if (session != null) { // 如果取到了Session this.requestedSessionIdValid = true; // 使用HttpSessionWrapper封装取到的Session, 因为向外界暴露的就是此类型 currentSession = new HttpSessionWrapper(session, getServletContext()); currentSession.setNew(false); setCurrentSession(currentSession); return currentSession; } else { // This is an invalid session id. No need to ask again if // request.getSession is invoked for the duration of this request if (SESSION_LOGGER.isDebugEnabled()) { SESSION_LOGGER.debug( "No session found by id: Caching result for getSession(false) for this HttpServletRequest."); } // 前端有SessionId传递过来, 却在外部存储中取不到时 setAttribute(INVALID_SESSION_ID_ATTR, "true"); } } // 默认情况下, 该create为true. // 这里的默认情况指代的是使用getSession()获取Session if (!create) { return null; } if (SESSION_LOGGER.isDebugEnabled()) { // 构造异常来获取栈信息 SESSION_LOGGER.debug( "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + SESSION_LOGGER_NAME, new RuntimeException( "For debugging purposes only (not an error)")); } // 创建一个Session, 注意这里的创建操作默认情况下还不会将Sessioni里的信息保存到外部存储中. // 这里默认是构建一个RedisSession类型的实例, 有一处细节是在RedisSession的构造函数中的flushImmediateIfNecessary()方法, 会按照RedisOperationsSessionRepository类中的redisFlushMode字段(RedisFlushMode枚举类型)值选择何时将redisSession中存储的信息推入Redis // 默认情况下实在"ON_SAVE"模式(具体参见枚举RedisFlushMode中的注释) S session = SessionRepositoryFilter.this.sessionRepository.createSession(); // 下面这步操作会最终导致Redis进行保存操作, 具体细节参见RedisOperationsSessionRepository$RedisSession类中的saveDelta方法实现. session.setLastAccessedTime(System.currentTimeMillis()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession;}
5. getRequestedSessionId()
方法
该方法的默认实现是由CookieHttpSessionStrategy
来完成的.
首先说明下:以下内容对于不了解Spring-Session约定的同学确实会造成理解上的困难, 所以我先尝试着解释一下:
1. 首先Spring-Session是支持单浏览器多用户Session的.
2. Spring-Session通过增加session alias概念来实现多用户session,每一个用户都映射成一个session alias。当有多个session时,spring会生成“alias1 sessionid1 alias2 sessionid2…….”这样的cookie值结构. 再说得具体点就是: 当你使用Spring-Session的这个功能时, Spring-Session会生成这样的一个Cookie : 键值默认为’SESSION’, 而value则是alias1 sessionid1 alias2 sessionid2…….”(其中alias1指代当前用户的标识性信息, 具体值由用户决定; 而sessionid1则是对应于alias1, 这个值用来与外部存储中的key进行匹配, 也就是说Spring-Session 会拿着这个sessionid1去外部容器中取出该alias1代表的用户Session; 如果有同一浏览器下有第二个用户登录, ‘SESSION’的值就会变成alias1 sessionid1 alias2 sessionid2”以此类推)
3. 第二步中的alias1是用户通过表单数据,或者url将 _s=xxx
传递给后台, 此时 alias1 就是 xxx.
4. 第二步中的sessionid1是由Spring-Session自主生成的唯一值. 最后由Spring-Session混合该 alias1和sessionid1; 最终拼接为 {SESSION: alias1 sessionid1}
的Cookie发送给前台.
5. 默认键值”SESSION” 位于 DefaultCookieSerializer
中,
6. 默认键值”_s” 位于 CookieHttpSessionStrategy
类中
7. 6. 默认键值”0” 位于 CookieHttpSessionStrategy
类中
// CookieHttpSessionStrategy类public String getRequestedSessionId(HttpServletRequest request) { // 从本次request中获取sessionId集合 Map<String, String> sessionIds = getSessionIds(request); // 从本次request获取当前对应的Session Alias String sessionAlias = getCurrentSessionAlias(request); // 返回外部存储中对应着当前用户的那个Session Id. // 首次登录时返回null return sessionIds.get(sessionAlias);}// CookieHttpSessionStrategy类public Map<String, String> getSessionIds(HttpServletRequest request) { // 这里的cookieSerializer默认实现为DefaultCookieSerializer List<String> cookieValues = this.cookieSerializer.readCookieValues(request); // 即使返回集合中有多个元素, 也只取第一个 String sessionCookieValue = cookieValues.isEmpty() ? "" : cookieValues.iterator().next(); Map<String, String> result = new LinkedHashMap<String, String>(); // 这里就解释了为什么"SESSION" cookie 中要以" "分割的 StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " "); if (tokens.countTokens() == 1) { // DEFAULT_ALIAS为 "0" // 加入默认用户 result.put(DEFAULT_ALIAS, tokens.nextToken()); return result; } while (tokens.hasMoreTokens()) { String alias = tokens.nextToken(); if (!tokens.hasMoreTokens()) { break; } String id = tokens.nextToken(); result.put(alias, id); } return result;}// DefaultCookieSerializer类对readCookieValues方法的实现public List<String> readCookieValues(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); List<String> matchingCookieValues = new ArrayList<String>(); if (cookies != null) { for (Cookie cookie : cookies) { // 这个this.cookieName默认值为"SESSION" if (this.cookieName.equals(cookie.getName())) { String sessionId = cookie.getValue(); if (sessionId == null) { continue; } // this.jvmRoute // Used to identify which JVM to route to for session affinity // can help with tracing logs of a particular user. if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) { sessionId = sessionId.substring(0, sessionId.length() - this.jvmRoute.length()); } matchingCookieValues.add(sessionId); } } } // 注意这里返回的是个集合 return matchingCookieValues;}// CookieHttpSessionStrategy类对getCurrentSessionAlias方法的实现public String getCurrentSessionAlias(HttpServletRequest request) { // this.sessionParam 默认值为 "_s" if (this.sessionParam == null) { return DEFAULT_ALIAS; } // 这里就解释了为什么在单浏览器多用户时, 需要在表单数据或者url中掺入"_s"的键值对 String u = request.getParameter(this.sessionParam); if (u == null) { return DEFAULT_ALIAS; } // 这里还有个正则匹配 "^[\\w-]{1,50}$" if (!ALIAS_PATTERN.matcher(u).matches()) { return DEFAULT_ALIAS; } return u;}// 拼接出前端对应的sessionid的值 即默认的 SESSION = xxxxxxxxx 中的xxxxxxxprivate String createSessionCookieValue(Map<String, String> sessionIds) { if (sessionIds.isEmpty()) { return ""; } if (sessionIds.size() == 1 && sessionIds.keySet().contains(DEFAULT_ALIAS)) { return sessionIds.values().iterator().next(); } // spring会生成“alias1 sessionid1 alias2 sessionid2…….”这样的cookie值结构。 StringBuffer buffer = new StringBuffer(); for (Map.Entry<String, String> entry : sessionIds.entrySet()) { String alias = entry.getKey(); String id = entry.getValue(); buffer.append(alias); buffer.append(" "); buffer.append(id); buffer.append(" "); } buffer.deleteCharAt(buffer.length() - 1); return buffer.toString();}
6. commitSession
方法
最后我们还需要将构建出的Session里的信息存储到Redis中.
/** * Uses the HttpSessionStrategy to write the session id to the response and persist the Session. */private void commitSession() { HttpSessionWrapper wrappedSession = getCurrentSession(); if (wrappedSession == null) { if (isInvalidateClientSession()) { // 触发Session失效事件 SessionRepositoryFilter.this.httpSessionStrategy .onInvalidateSession(this, this.response); } } else { S session = wrappedSession.getSession(); // 保存 SessionRepositoryFilter.this.sessionRepository.save(session); if (!isRequestedSessionIdValid() || !session.getId().equals(getRequestedSessionId())) { // 触发Session新建事件 SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session, this, this.response); } }}
7.RedisSession.saveDelta
方法
RedisOperationsSessionRepository$RedisSession
类中的saveDelta
方法实现.
/** * Saves any attributes that have been changed and updates the expiration of this * session. */private void saveDelta() { // 这里的判断的依据是 除非你显式调用了session的setAttribute, removeAttribute,setMaxInactiveIntervalInSeconds,setLastAccessedTime方法, 否则spring-session不会将数据推送到redis // 而在 SessionRepositoryRequestWrapper 覆写的getSession方法中,只有在创建session时才会调用setLastAccessedTime方法, 按照上面的结论该新建的session会被推送到redis // 但是使用本方法从redis取到的session里提取出来的数据,针对该数据做出的修改不会被推送到redis; 如果你想要它推送会redis, 那就麻烦你再次调用上面的四个方法中的某一个一次(这里的四个针对spring-session-1.2.0.RELEASE.jar版本) if (this.delta.isEmpty()) { return; } String sessionId = getId(); // 以下这一句就是以 "spring:session:" + "sessions:" + sessionId 为key 将值推入redis;;; 已测试 getSessionBoundHashOperations(sessionId).putAll(this.delta); String principalSessionKey = getSessionAttrNameKey( FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); String securityPrincipalSessionKey = getSessionAttrNameKey( SPRING_SECURITY_CONTEXT); if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) { if (this.originalPrincipalName != null) { String originalPrincipalRedisKey = getPrincipalKey( this.originalPrincipalName); RedisOperationsSessionRepository.this.sessionRedisOperations .boundSetOps(originalPrincipalRedisKey).remove(sessionId); } String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this); this.originalPrincipalName = principal; if (principal != null) { String principalRedisKey = getPrincipalKey(principal); RedisOperationsSessionRepository.this.sessionRedisOperations .boundSetOps(principalRedisKey).add(sessionId); } } this.delta = new HashMap<String, Object>(this.delta.size()); Long originalExpiration = this.originalLastAccessTime == null ? null : this.originalLastAccessTime + TimeUnit.SECONDS .toMillis(getMaxInactiveIntervalInSeconds()); RedisOperationsSessionRepository.this.expirationPolicy .onExpirationUpdated(originalExpiration, this);}
Links
- http://blog.csdn.net/szwandcj/article/details/50304831
- http://blog.csdn.net/szwandcj/article/details/50319901
- Spring-Session源码研究之processRequest
- Spring-Session源码研究之Start
- Spring-Session源码研究之Start_Servlet3.0
- Spring源码研究之@Configuration
- ProcessRequest
- spring源码研究之路_IOC
- spring源码研究(0)
- spring-session源码解析
- Spring源码研究之注解扫描<context:component-scan/>
- Mybatis与Spring集成源码研究之MapperScannerConfigurer
- ViewState与Session之研究
- ViewState与Session之研究
- Spring AOP源码研究笔记
- 研究spring源码有所得
- spring-session源码解读-1
- spring-session源码解读-2
- spring-session源码解读-3
- spring-session源码解读-4
- [App] Squid 代理缓存服务器安装
- Ubuntu 17.10安装sklearn
- c++基础知识
- [LeetCode] 669.Trim a Binary Search Tree
- 版本控制之git命令行大全(分支教程)
- Spring-Session源码研究之processRequest
- 【UI设计】4、RecycleView的使用
- struts2的<s:property />的使用
- idea同步svn中的maven项目,配置项目(带Spring Boot启动类)
- TQ210裸机编程——printf
- 我的电路实验
- 图形登陆界面
- 按钮布局演示
- Spark的左外连接解决方案