spring session源码理解

1.传统session与spring session的比较

在传统session模式中,客户端发送请求,通过cookie上存储的sessionid在服务器上找到对应的session,然后进行其他操作。

而在springsession模式中,session就不是存储在服务器上的,而是存储在一个独立的存储服务器上,所有请求获取session都是从这个独立的存储服务器上获取。

在单机情况下两种模式都没什么问题,但是在集群环境下传统session的问题就很明显了,传统session模式下每台服务器只能获取自身存储的session而不能获取其他服务器的session。springsession模式下session都存储在一个独立存储服务器,任何一台服务器都可以获取到所有的session。

2.spring session源码解析

1.spring session基本流程

spring session目前支持MySql、Redis、Mongodb等多种session存储形式,这里以Redis为例分析

@EnableRedisHttpSession注解导入了RedisHttpSessionConfiguration配置,RedisHttpSessionConfiguration会注入以下两个组件:

1.RedisHttpSessionConfiguration给容器中添加了一个组件SessionRepository,其实现类为 【RedisOperationsSessionRepository】,所有redis的操作都由该组件完成。如session的增删改查操作。

sessionRepository注入代码片段如下:

@Bean
public RedisOperationsSessionRepository sessionRepository() {
    RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
    ...
    return sessionRepository;
}

2.RedisHttpSessionConfiguration还会给容器注入SessionRepositoryFilter ==》Filter: session存储过滤器;

SessionRepositoryFilter注入代码片段如下:

@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
    sessionRepositoryFilter.setServletContext(this.servletContext);
    sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
    return sessionRepositoryFilter;
}

每个请求过来都必须经过SessionRepositoryFilter ,其主要功能如下: 1.创建的时候,就自动从容器中获取到了SessionRepositroy; 2.原始的request,response都被包装成SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper 3.以后获取session。request.getSession();就是SessionRepositoryRequestWrapper.getSession();

而SessionRepositoryRequestWrapper会从SessionRepository中获取到的session,就实现了从独立的session存储服务器上获取session

2.spring session源码分析

1.RedisOperationsSessionRepository

@Bean
public RedisOperationsSessionRepository sessionRepository() {
    // 创建redisTemplate
    RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
    RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
    sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
    // 设置默认redis序列化
    if (this.defaultRedisSerializer != null) {
        sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
    }
	// 设置session默认的最大有效时间
    sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
    // 设置命名空间,默认为spring:session,在redis中存储session的key以命名空间为前缀
    if (StringUtils.hasText(this.redisNamespace)) {
        sessionRepository.setRedisKeyNamespace(this.redisNamespace);
    }

    sessionRepository.setRedisFlushMode(this.redisFlushMode);
    int database = this.resolveDatabase();
    sessionRepository.setDatabase(database);
    return sessionRepository;
}

springsession中对redis操作都是通过该类完成

session在redis中的存储命名如下:

127.0.0.1:6379> keys *
1) "spring:session:sessions:00d50fcb-b832-476f-a762-c414823d7de1"
2) "spring:session:expirations:1687857240000"
3) "spring:session:sessions:expires:00d50fcb-b832-476f-a762-c414823d7de1"

2.SessionRepositoryFilter

@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
    sessionRepositoryFilter.setServletContext(this.servletContext);
    sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
    return sessionRepositoryFilter;
}

RedisHttpSessionConfiguration通过其父类SpringHttpSessionConfiguration完成SessionRepositoryFilter的注入。

SessionRepositoryFilter拦截方法如下:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 为请求属性上设置sessionRepository
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
    // 包装原始请求为SessionRepositoryRequestWrapper
    SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
    // 包装原始响应为SessionRepositoryResponseWrapper
    SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

    try {
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    } finally {
        // 提交session
        wrappedRequest.commitSession();
    }
}

SessionRepositoryFilter完成对请求和响应的包装,使用了装饰者模式。

此后的request操作其实都是对SessionRepositoryRequestWrapper进行操作的。

finally中提交session是对session数据持久化到redis的操作。

3.SessionRepositoryRequestWrapper

接下来就看SessionRepositoryRequestWrapper到底把原始request封装成什么样了。

private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
    // 原始响应
    private final HttpServletResponse response;
    private final ServletContext servletContext;
    // 该请求的redisSession
    private S requestedSession;
    private boolean requestedSessionCached;
    // 该请求sessionId
    private String requestedSessionId;
    private Boolean requestedSessionIdValid;
    private boolean requestedSessionInvalidated;
    ...

SessionRepositoryRequestWrapper继承了HttpServletRequestWrapper,HttpServletRequestWrapper遵循HttpServletRequest接口规范,可以发现SessionRepositoryRequestWrapper只是重写了有关session的方法,其他的方法还是使用原请求方法。

其重写的getSession(boolean)方法如下:

public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
    // 从当前请求的attribute中获取session,如果有直接返回
    SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
    if (currentSession != null) {
        return currentSession;
    } else {
        // 根据请求cookie的sessionId获取存在redis中session
        S requestedSession = this.getRequestedSession();
        if (requestedSession != null) {
            if (this.getAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {
                // 设置最后访问时间
                requestedSession.setLastAccessedTime(Instant.now());
                this.requestedSessionIdValid = true;
                // 对redisSession进行封装以符合HttpSession接口规范(使用了适配器模式)
                currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, this.getServletContext());
                currentSession.setNew(false);
                // 设置session至Requset的attribute中,提高同一个request访问session的性能
                this.setCurrentSession(currentSession);
                return currentSession;
            }
        } else {
            this.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");
        }

        // 当前没有session时是否创建session
        if (!create) {
            return null;
        } else {
            // 创建新session
            S session = SessionRepositoryFilter.this.sessionRepository.createSession();
            // 设置最后访问时间
            session.setLastAccessedTime(Instant.now());
            // 对redisSession进行封装以符合HttpSession接口规范
            currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, this.getServletContext());
            // 设置session至Requset的attribute中,提高同一个request访问session的性能
            this.setCurrentSession(currentSession);
            return currentSession;
        }
    }
}

getsession()会获取到一个springsession自己封装的session,可以看下该session的结构

以上主要属性是cached和delta,cached用于存储session的全量值,获取值从cached中获取提高效率。delta则用于存储新增、删除、修改的值用于持久化到redis中。

4.SessionRepositoryResponseWrapper

private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {
    private final SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;

    SessionRepositoryResponseWrapper(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {
        super(response);
        if (request == null) {
            throw new IllegalArgumentException("request cannot be null");
        } else {
            this.request = request;
        }
    }

    protected void onResponseCommitted() {
        // 提交session(持久化)
        this.request.commitSession();
    }
}

SessionRepositoryResponseWrapper就重写了onResponseCommitted()这一个方法,作用是持久化session,确保在response提交前session被持久化到redis。我测试过这个持久化要先于SessionRepositoryFilter.doFilterInternal中finally里的持久化。

接下来看一下如何持久化到redis的

private void commitSession() {
    SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();
    if (wrappedSession == null) {
        if (this.isInvalidateClientSession()) {
            SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
        }
    } else {
        // 取出redisSession
        S session = wrappedSession.getSession();
        this.clearRequestedSessionCache();
        // 使用sessionRepository持久化到redis
        SessionRepositoryFilter.this.sessionRepository.save(session);
        String sessionId = session.getId();
        // 如果是新创建spring session,sessionId添加到response的cookie
        if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {
            // 该方法里面sessionId会进行base64编码,我开始还奇怪为啥cookie存的和redis存的sessionId不一样呢
            SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
        }
    }
}

RedisOperationsSessionRepository.save()

public void save(RedisOperationsSessionRepository.RedisSession session) {
    // 将RedisSession中delta存储的变化的数据持久化到redis
    session.saveDelta();
    if (session.isNew()) {
        String sessionCreatedKey = this.getSessionCreatedChannel(session.getId());
        this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
        session.setNew(false);
    }
}

5.HttpSessionWrapper

接下来看下HttpSessionWrapper,在我们操作session.setAttribute()和session.getAttribute()它是怎么实现的,这里需要结合上面的session的结构图看。

public void setAttribute(String name, Object value) {
    this.checkState();
    Object oldValue = this.session.getAttribute(name);
    // 设置属性
    this.session.setAttribute(name, value);
    if (value != oldValue) {
        if (oldValue instanceof HttpSessionBindingListener) {
            try {
                ((HttpSessionBindingListener)oldValue).valueUnbound(new HttpSessionBindingEvent(this, name, oldValue));
            } catch (Throwable var6) {
                logger.error("Error invoking session binding event listener", var6);
            }
        }

        if (value instanceof HttpSessionBindingListener) {
            try {
                ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this, name, value));
            } catch (Throwable var5) {
                logger.error("Error invoking session binding event listener", var5);
            }
        }
    }
}

RedisSession.setAttribute

public void setAttribute(String attributeName, Object attributeValue) {
    // 设置属性到cached中,以后get可以直接从cached获取,提高效率
    this.cached.setAttribute(attributeName, attributeValue);
    // 方法里面就是属性值设置到delta中,跟踪变化值
    this.putAndFlush(RedisOperationsSessionRepository.getSessionAttrNameKey(attributeName), attributeValue);
}

springsession源码解析就到这里了,由于本人技术有限,如有不对之处还望指出,本人定虚心修正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值