Spring-Session源码研究之processRequest

来源:互联网 发布:wap页面纯文字游戏源码 编辑:程序博客网 时间:2024/05/21 08:48

之前的两章里面我们分别讨论了Spring-Session的配置这一块. 讨论其是如何将自己并入到Servlet生命周期中的, 以及在并入时如何准备自身正常工作所需要的条件.

而本章节讲解Spring-Session在一次完整的请求过程中是如何处理Session相关的问题.

所以我们的关注重点是 SessionRepositoryFilter<S extends ExpiringSession>

1. 概述

由上一篇文章我们已经了解到了, SessionRepositoryFilter就是作为处理请求的第一个Filter, 在其内部实现中会将tomcat等servlet容器对ServletRequestServletResponse的实现使用Wrapper模式, 以加入自定义的逻辑——共享session.

2. OncePerRequestFilter

  1. SessionRepositoryFilter扩展自OncePerRequestFilter.
  2. OncePerRequestFilter类看名称就能猜到本类封装的是保证本Filter在一次完整的拦截链中只执行一次的逻辑, 因为spring无法保证同一个Filter类只有一个实例。有可能一个Filter既有可能在web.xml里配置由容器初始化了,还有可能被作为spring的依赖引入进了DelegatingFilterProxy。这样在一次filter chain中就会存在同一个Filter的多个实例, 所以OncePerRequestFilter类通过向request参数中插入一个Boolean类型的标识来确保本类Filter只执行一次(不论本拦截链有多个本类型的实例).
  3. , OncePerRequestFilter类对接口Filter的实现采用了final, 而将扩展途径以doFilterInternal开放给子类.

3. doFilterInternal方法

接下来我们就来看看SessionRepositoryFilterdoFilterInternal的实现

@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);}
  1. http://blog.csdn.net/szwandcj/article/details/50304831
  2. http://blog.csdn.net/szwandcj/article/details/50319901
原创粉丝点击