目录

  • 一、什么是session
  • 1. session定义
  • 2. session的机制
  • 二、标准session实现和spring-session的区别
  • 三、spring-session与springboot集成
  • 1. 引入依赖包
  • 2. 创建httpSession配置文件
  • 3. 根据实际情况个性化redis配置
  • 四、spring-session核心流程解析
  • 1. SessionRepositoryFilter的包装
  • 2. spring-session对request的包装SessionRepositoryRequestWrapper
  • 五、总结


一、什么是session

1. session定义

根据Oracle的java servlet 4.0标准,可以找到对sessiont的明确定义:session是在http多次回话中,与client绑定的机制,用于构建有效的web应用,追踪用户的行为。由于http协议是无状态协议,所以session的存在对用户状态绑定非常重要。在上述提到的标准协议里,制定了HttpSession的标准接口,容器(例如tomcat)和各种网络框架都有对这个标准接口的实现。

2. session的机制

同时,协议里也规定了几种标准的session机制,分别是:
cookies: cookies机制是最常用的,同时也要求全部的servlet容器加以实现。容器会将一个cookie随response body一起返回给client,之后的每个请求头里面,client都会将cookie里的值加进去。
SSL Session: SSL回话中,有内建的机制来保证来自一个client中的多个请求,用作session的一部分。
URL Rewriting: 这种方法是很不常用的,当一个client禁用cookie的时候,URL重写会被服务器用作session机制。用于将不同的请求与session相关联。例如:
http://www.example.com/catalog/index.html;jsessionid=1234

二、标准session实现和spring-session的区别

根据Oracle的协议规范,我们知道,web容器需要实现session机制,但是web容器是将其存储在内存之中的。这样,在分布式的应用中,session也就只能在单台机器上有效。作为解决方案之一,我们可以使用spring-session,将session从内存中持久化到外部存储。用一张图可以清晰地表示出他们之间的区别:

JSESSIONID 为什么会变JSESSIONID java jsessionid_spring

三、spring-session与springboot集成

由于springboot开箱即用,集成起来非常简单,只需要三步(我们这里以使用redis存储为例)

1. 引入依赖包

<dependencies>
		<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-redis</artifactId>
		</dependency>
	</dependencies>

2. 创建httpSession配置文件

package example.helloworld;

import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@EnableRedisHttpSession
public class HttpSessionConfig {
	
}

3. 根据实际情况个性化redis配置

spring:
  redis:
      host: 127.0.0.1
      port: 6380

四、spring-session核心流程解析

spring-session的机制很巧妙,主要是应用责任链模式和装饰器模式,在容器的filter_chain中插入session的过滤器SessionRepositoryFilter,在这里将request进行包装,把装饰后的request向下传递。用一个示意图可以进行描述:

JSESSIONID 为什么会变JSESSIONID java jsessionid_持久化_02


接下来对请求过程中session相关的核心流程进行分析

1. SessionRepositoryFilter的包装

SessionRepositoryFilter的核心操作如下:

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		//根据配置设置session的持久化策略
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

        //包装request和response为spring-session支持的类型
		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
				response);
        //filterChain向下传递请求
		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
		    //提交session,即将sessionId写入返回结果,并持久化session
			wrappedRequest.commitSession();
		}
	}

2. spring-session对request的包装SessionRepositoryRequestWrapper

spring-session通过使用装饰器模式,来对session相关方法进行个性化。SessionRepositoryRequestWrapper的UML图如下所示:

JSESSIONID 为什么会变JSESSIONID java jsessionid_持久化_03


getSession:

get是tomcat提供的HttpServletRequest的接口,wrapper对其的具体实现如下:

//create表示在当前请求没有session的时候是否创建session
public HttpSessionWrapper getSession(boolean create) {
            //从内存中获取session----》关键
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}
			//从缓存和外部存储中获取session----》关键
			S requestedSession = getRequestedSession();
			if (requestedSession != null) {
				if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
					//设置session最近活跃时间
					requestedSession.setLastAccessedTime(Instant.now());
					this.requestedSessionIdValid = true;
					currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
					//将session设置为not new,这里要注意一下,判断一个session是否是new,取决于这                           个session是否有client join过
					currentSession.markNotNew();
					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
				setAttribute(INVALID_SESSION_ID_ATTR, "true");
			}
			if (!create) {
				return null;
			}
			//根据session存储模式创建与之相适配的session
			S session = SessionRepositoryFilter.this.sessionRepository.createSession();
			//设置session活跃时间
			session.setLastAccessedTime(Instant.now());
			//创建currentSession
			currentSession = new HttpSessionWrapper(session, getServletContext());
			//将其放入request的atrributes map中
			setCurrentSession(currentSession);
			return currentSession;
		}

这里的获取session分为两段,有点像分级缓存。其中两个核心的方法如下:
从request的attributes中获取:

private HttpSessionWrapper getCurrentSession() {
			return (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR);
		}

从外部存储空间获取:

private S getRequestedSession() {
			if (!this.requestedSessionCached) {
			   //根据配置的sessionId解析器来从请求中解析sessionId,默认的session策略是cookies,所以这里会从cookie中读取sessionId的值,key是SESSION
				List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
				for (String sessionId : sessionIds) {
					if (this.requestedSessionId == null) {
						this.requestedSessionId = sessionId;
					}
					//根据session的存储策略,从外部存储空间中读取session
					S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
					if (session != null) {
						this.requestedSession = session;
						this.requestedSessionId = sessionId;
						break;
					}
				}
				this.requestedSessionCached = true;
			}
			return this.requestedSession;
		}

commitSession
在filterChain路由结束之后,回到SessionRepositoryFilter的时候,会对session进行提交,在这里,也就是指在redis中进行持久化,代码如下:

private void commitSession() {
			HttpSessionWrapper wrappedSession = getCurrentSession();
			if (wrappedSession == null) {
			    //session不可用
				if (isInvalidateClientSession()) {
				    //变更session状态,提示客户端,对于cookies策略,这里会把cookie里的sessionId清空
					SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
				}
			}
			else {
			    //获取原生的HttpSession
				S session = wrappedSession.getSession();
				clearRequestedSessionCache();
				//持久化session
				SessionRepositoryFilter.this.sessionRepository.save(session);
				String sessionId = session.getId();
				if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
					SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
				}
			}
		}

总之,session创建和提交过程中,会根据配置中读取到的session存储模式和session策略来对session进行实例化,例如使用redis,实例化的就是RedisSession,通过抽象来实现解耦。

五、总结

通过上述的分析可知:
1、使用springboot集成spring-session,经过简单的个性化配置即可支持分布式session存储,对用户透明,是否友好;
2、spring-session内部通过责任链和装饰器模式,对tomcat原生的httpRequest进行包装,与其他的机制完美兼容;
3、不同的存储策略对应不同的session子类,便于解耦和拓展。