spring session

官方文档

 

当我们把单体 web 应用拆分成分布式 web 应用时,一个不得不面对的问题就是如何处理 session。我们一般的做法不使用 HttpServletRequest,使用分布式存储数据库来存储 session,用的比较多的方案是使用 redis 来存储 sesion 。这样做没什么问题,但是需要修改业务代码,增加了开发和维护成本。

对于这个问题 spring-session 提供了一整套的解决方案。它的好处就是你不需要修改你的代码,你依然可以使用原来的 HttpServletRequest 来处理 session。它支持多种外部存储:Hazelcast,Redis,JDBC。当然你也可以使用它的基础类实现其他存储方案。以下我会以 Redis 作为例子讲解。

重要的几个类

  • SessionRepository:管理 session 的一个仓库,包括 create,save(持久化),delete等等。

  •  

主要的类实现类就是我用红线框出来的类。以下是对这些类的简单说明:

  1. RedisIndexedSessionReponsitory:可发布事件的 redis 仓库,它还额外实现了 MessageListener,这也意味着它会使用 redis 的键空间(你的 redis 必须要开启键空间的功能)。它会在 redis 上生成三条数据。一个是以 spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 的 hash;一个是用来记录 session 是否有效的 string;一个是用来对 redis 键空间补偿(因为键空间不是特别的靠谱(如果不访问键的话可能不会出发键空间),采用的方式是每分钟轮询(具体请看 RedisSessionExpirationPolicy))的 Set,它的 key 是 spring:session:expirations:当前时间(秒和毫秒都是0)的下一分钟的时间戳,里面存储的是这一分钟内生成的 sessionId。还可以自定义序列化,默认时使用。具体可以看官方API
  2. RedisSessionRepository:简单版的 redis session 方案,相较于 RedisIndexedSessionReponsitory 它没有实现 MessageListener 也不能发布事件,它在 redis 上只会生成一条数据。如果你不需要对键过期或删除做后续处理,你就可以使用这个存储方案。
  3. JdbcIndexedSessionRepository:使用 jdbc 作为外部存储,要先建表,速度也是一个问题,成本较高,不太推荐。
  4. HazelcastIndexedSessionRepository:使用 Hazelcast 作为外部存储。Hazelcast 是一个内存数据网格(IMDG),也是 kv 存储,但是国内好像用的比较少。这个真的蛮好用的,一个 jar 就可以部署。
  • org.springframework.session.Session:存储并操作 session。上面的每一个 SessionRepository 实现类基本都有一个 Session 的内部类。
  • MapSession:它是 Session 的一个实现类也是真正存储 session 的类。Session 实现类内部都是使用 MapSession 作为委托类存储的。sessionId 的生成方案使用的是 UUID。
  • SessionRepositoryFilter:一个 web 过滤器。非常中要的一个类。SessionRepository 需要结合它才能发挥作用。它的构造方法接收一个 SessionRepository。通过 @EnableSpringHttpSession 引入。

此外它还支持响应式的 redis 存储方案。

具体运行过程

以 RedisIndexedSessionReponsitory 为例。

  1. 走 SessionRepositoryFilter 的 doFilterInternal 方法。

 

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
            response);
    try {
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    }
    finally {
        wrappedRequest.commitSession();
    }

SessionRepositoryRequestWrapperSessionRepositoryResponseWrapper 分别对 request 和 response 包装。SessionRepositoryRequestWrapper 的几个重要方法:commitSession(),getSession()

 

// 主要在响应返回时被调用,它的作用是往响应中写 cookie 和 持久化 session
private void commitSession() {
    HttpSessionWrapper wrappedSession = getCurrentSession();
    if (wrappedSession == null) {
        if (isInvalidateClientSession()) {
            SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
        }
    }
    else {
        S session = wrappedSession.getSession();
        clearRequestedSessionCache();
        SessionRepositoryFilter.this.sessionRepository.save(session);
        String sessionId = session.getId();
        if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
            SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
        }
    }
}

// 获取 session 这也是为什么使用 spring-session 后不需要修改原有代码的原因
@Override
public HttpSessionWrapper getSession() {
    return getSession(true);
}

@Override
public HttpSessionWrapper getSession(boolean create) {
    HttpSessionWrapper currentSession = getCurrentSession();
    if (currentSession != null) {
        return currentSession;
    }
    S requestedSession = getRequestedSession();
    if (requestedSession != null) {
        if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
            requestedSession.setLastAccessedTime(Instant.now());
            this.requestedSessionIdValid = true;
            currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
            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
        if (SESSION_LOGGER.isDebugEnabled()) {
            SESSION_LOGGER.debug(
                    "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
        }
        setAttribute(INVALID_SESSION_ID_ATTR, "true");
    }
    if (!create) {
        return null;
    }
    if (SESSION_LOGGER.isDebugEnabled()) {
        SESSION_LOGGER.debug(
                "A new session was created..."
                        + SESSION_LOGGER_NAME,
                new RuntimeException("For debugging purposes only (not an error)"));
    }
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    session.setLastAccessedTime(Instant.now());
    currentSession = new HttpSessionWrapper(session, getServletContext());
    setCurrentSession(currentSession);
    return currentSession;
}

private S getRequestedSession() {
    if (!this.requestedSessionCached) {
        List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
        for (String sessionId : sessionIds) {
            if (this.requestedSessionId == null) {
                this.requestedSessionId = sessionId;
            }
            S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
            if (session != null) {
                this.requestedSession = session;
                this.requestedSessionId = sessionId;
                break;
            }
        }
        this.requestedSessionCached = true;
    }
    return this.requestedSession;
}
  1. 用户代码中调用 HttpServletRequest#getSession(),就像这样:

 

@RestController
@RequestMapping(value = "/")
public class SessionController {

    @RequestMapping(value = "/session")
    public Map<String, Object> getSession(HttpServletRequest request) {
        Random random = new Random();
        // getSession() 实际上调用的是 SessionRepositoryRequestWrapper#getSession()
        request.getSession().setAttribute("userName" + random.nextInt(100), "glmapper");
        Map<String, Object> map = new HashMap<>();
        map.put("sessionId", request.getSession().getId());
        return map;
    }
}

如何使用

使用的话秉承了 Spring 一贯的开箱即用原则,那是相当的简单啊。只要加上对应的 @Enable... 就可以了,具体看官方给的例子吧。

 

 

  1. spring-session-sample-boot-redis:使用 RedisIndexedSessionRepository 作为仓库并且使用 JDK 序列化。
  2. spring-session-sample-boot-redis-json:使用 RedisIndexedSessionRepository 作为仓库并且使用 json 序列化。
  3. spring-session-sample-boot-redis-simple:使用 RedisSessionRepository 作为仓库

总结

对于分布式 web 应用 spring-session 真的是一个非常好的选择。无论时接入还是开发成本都非常低,配合 spring-security 就是两个字完美,官方也给出了结合 security 的方案,大家可以去翻阅源码。

 

posted @   車輪の唄  阅读(38)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2020-02-28 kafka开启sasl认证
点击右上角即可分享
微信分享提示