【每天学点Spring】Spring Session深入学习

【前置文章】

1. Spring Session是如何工作的?

1.1 创建了Bean: SessionRepository

Source code: github

本章节重要的类如下:


SessionRepository以及它的实现类

【以下是装配Hazelcast4IndexedSessionRepository的过程】
首先,配置类加载的annotation:@EnableHazelcastHttpSession --> 点进去可以看到引用了:@Import(HazelcastHttpSessionConfiguration.class)

这个类本身,有个方法:sessionRepository(),hazelcast从3.0+到4.0+,升级后,IMap所在的类的包名也换过了:

  • 3.0+,IMap位于:com.hazelcast.core.IMap --> hazelcast4 = false
  • 4.0+ & 5.0+,IMap位于:com.hazelcast.map.IMap --> hazelcast4 = true
static {
    ClassLoader classLoader = HazelcastHttpSessionConfiguration.class.getClassLoader();
    hazelcast4 = ClassUtils.isPresent("com.hazelcast.map.IMap", classLoader);
}

@Bean
public FindByIndexNameSessionRepository<?> sessionRepository() {
    if (hazelcast4) {
        return createHazelcast4IndexedSessionRepository();
    } else {
        return createHazelcastIndexedSessionRepository();
    }
}

我们用的是5.1+,所以最终创建的是:Hazelcast4IndexedSessionRepository.java,这个类主要负责将session持久化到Hazelcast分布式Map中:

A SessionRepository implementation that stores sessions in Hazelcast's distributed IMap.

简单介绍下save(session)方法:

  • 将每个单独的对象以sessionID为key的形式,session作为value,存放到Hazelcast sessions这个分布式IMap中。
  • sessions这个IMap的名字为spring:session:sessions
  • session存放到Hazelcast的时候,是有expire时间的,即getMaxInactiveInterval()
private IMap<String, MapSession> sessions;

@PostConstruct
public void init() {
    this.sessions = this.hazelcastInstance.getMap(this.sessionMapName);
    this.sessionListenerId = this.sessions.addEntryListener(this, true);
}

public void save(HazelcastSession session) {
    if (session.isNew) {
        this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);
    }
    // 略
}
1.2 通过SessionRepositoryFilter拦截请求并存放session

如果说上述的#1.1介绍的Hazelcast4IndexedSessionRepository类负责如何将session存放到Hazelcast中,以便可以在不同的Tomcat中共享session对象,那么当一个request请求进入后台的时候,Spring Session是如何触发session的存放的呢?

这时候可以查看位于spring-session-core中的类:【SessionRepositoryFilter】。本质上来说,它是一个Filter,那么就会执行熟悉的doFilter方法--> 找到doFilter最终调用了:doFilterInternal(req, res, chain)

doFilterInternal()方法主要有以下逻辑:

  • HttpServletRequest封装了一个wapper:SessionRepositoryRequestWrapper
  • HttpServletResponse封装了一个wapper:SessionRepositoryResponseWrapper
  • 在做完filterChain的doFilter后,也就是request完成后,有个重要方法是:wrappedRequest.commitSession();
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();
    }
}

重点看下方法:wrappedRequest.commitSession()

  • a. 首先从当前的request中取出session,对象为wrapped session。

  • b. 如果当前session为空,那么就expire。
    expire的逻辑为,在上述第#2章可知,session创建后会返回给浏览器的response中加上Set-Cookie: JSESSIONID=xxx; 而spring-session重写了该header,默认为Set-Cookie: SESSION=xxx; 当session为空,那么就会把这个值设为空。

  • c. 如果当前session不为空,从wrapped session中取出session对象,再调用:**SessionRepositoryFilter.this.sessionRepository.save(session); **--> 即#1.1中调用save的地方(持久化到Hazelcast中)
    this.httpSessionIdResolver.setSessionId即将sessionID放到response中,为:Set-Cookie: SESSION=xxx

    image.png

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);
        }
    }
}
1.3 通过SessionRepositoryRequestWrapper创建session

上述#1.2 SessionRepositoryFilter类中的内部类:SessionRepositoryRequestWrapper,除了上述介绍的重要方法commitSession()外,还有一个重要方法,即HttpSessionWrapper getSession(boolean create),取出当前session。

具体来说,即:

  • a. getCurrentSession(): 先从request中getAttribute,按key=SessionRepository.class.getName().CURRENT_SESSION(什么时候set的?已经取出过一次,或create的时候set的)。 --> 有则返回。

  • b. 如果request.attribute中为空,则调用getRequestedSession(),即从request的Cookie: SESSION=xxx中读取sessionID,然后通过sessionRepository.findById(sessionId),从具体的持久化类中查按sessionID查找出对象,在我们的示例中,这个持久化类为#4.1中介绍的类。取出session对象后,放入request.attribute中(这样下一次被调用就可以直接从#a的逻辑中拿到session对象,而不是每一次都从hazelcast中取)。

    image.png

  • c. 如果还没有,则跟据create的boolean值判断是否需要新创建一个。

2. 具体来看session对象是如何wrap的

在#1.1中介绍的Hazelcast4IndexedSessionRepository类中,它的方法有:

  • void save(HazelcastSession session)
  • HazelcastSession findById(String id)

可以看到输入和输出,都是HazelcastSession,以下是定义,它实现了org.springframework.session.Session接口:

final class HazelcastSession implements Session {
    private final MapSession delegate;
    private boolean isNew;
    private boolean sessionIdChanged;
    private boolean lastAccessedTimeChanged;
    private boolean maxInactiveIntervalChanged;
    private String originalId;
    private Map<String, Object> delta = new HashMap<>();
    // 略

【问题】我们具体在controller中用的request.getSession(),取出来的对象应该为HttpSession,而spring-session给我们持久化的时候,取出的session却为spring提供的Session对象。这中间是如何转换的呢?

首先,通过#1.2我们得知,request在filter层被wrapped过了,即:SessionRepositoryRequestWrapper

比如我们调试下:
image.png

request.getSession() --> 调用的是#1.3介绍的方法:HttpSessionWrapper getSession(boolean create)

这里返回的HttpSessionWrapper,继承了类:HttpSessionAdapter

private final class HttpSessionWrapper extends HttpSessionAdapter<S> {
}

HttpSessionAdapter最终实现了接口HttpSession,而这个adapter类中存放的正是Spring的接口Session的实现类!!!
由此得出结论:【HttpSessionAdapter类的作用为:Spring Session和原生的servlet里的HttpSession桥接】

class HttpSessionAdapter<S extends Session> implements HttpSession {
    private S session;
}

【总结】
a. 在hazelcast中查询出session,此时的session为HazelcastSession(实现的接口为Spring的Session)
b. 通过new HttpSessionWrapper(session, servletContext); 将上述的session,存放到HttpSessionAdapter中。
c. HttpSessionAdapter实现了HttpSession接口,所以只需要把HttpSession接口中的方法,用session中的方法实现即可。
比如:HttpSession.getAttribute(name),在桥接类中:替换为具体的Spring session接口类:

@Override
public Object getAttribute(String name) {
    checkState();
    return this.session.getAttribute(name);
}

有点偏题,总之通过经典的设计模式,spring-session实现了对使用方来说透明的session操作。

3. Session更新策略之FlushMode

在EnableHazelcastHttpSession中,github源码,有以下两个定义:

  • FlushMode flushMode() default FlushMode.ON_SAVE;
  • SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;

本章讨论FlushMode对session save的影响,FlushMode有两个值:

  • ON_SAVE(默认),只会在sessionRepository.save(session)被调用的时候,session才会被持久化到Hazelcast中,在web项目中,即是http返回给浏览器之前的commitSession(),看上述的#1.2章。

  • IMMEDIATE,立刻持久化session,比如在以下方法中session就会立刻被存到Hazelcast中:
    - sessionRepository.createSession()
    - void setLastAccessedTime(Instant lastAccessedTime)
    - void setMaxInactiveInterval(Duration interval)
    - void setAttribute(String attributeName, Object attributeValue)

4. Session更新策略之SaveMode

本章讨论SaveMode对session save的影响。

4.1 save(session) 源码

在#1.1中介绍的HazelcastIndexedSessionRepositorysave(session)方法,具体来看:

public void save(HazelcastSession session) {
    if (session.isNew) {
        this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);

    } else if (session.sessionIdChanged) {
        this.sessions.delete(session.originalId);
        session.originalId = session.getId();
        this.sessions.set(session.getId(), session.getDelegate(), session.getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS);

    } else if (session.hasChanges()) {
        Hazelcast4SessionUpdateEntryProcessor entryProcessor = new Hazelcast4SessionUpdateEntryProcessor();
        if (session.lastAccessedTimeChanged) {
            entryProcessor.setLastAccessedTime(session.getLastAccessedTime());
        }
        if (session.maxInactiveIntervalChanged) {
           
 entryProcessor.setMaxInactiveInterval(session.getMaxInactiveInterval());
            }
        if (!session.delta.isEmpty()) {
            entryProcessor.setDelta(new HashMap(session.delta));
        }
        this.sessions.executeOnKey(session.getId(), entryProcessor);
    }
    session.clearChangeFlags();
}
4.2 session整个object更新

上述源码中,前两个条件都会触发一次整个session object往Hazelcast中持久化:

  • 条件1【if (session.isNew)】,session是新创建的,所以直接调用IMap对象sessions来放入hazelcast中。适用的case如:在业务代码中首次调用request.getSession()方法。即会create session的场景,在createSession()的时候,会将isNew置为true。
    HazelcastSession session = new HazelcastSession(cached, true);

  • 条件2【else if (session.sessionIdChanged)】,sessionIdChanged应该是servlet request的原生方法触发的,具体查看文档:https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html#changeSessionId,简单理解即sessionID需要换,所以代码中是先在hazelcast中删除了老的ID,然后再将新的ID作为key,存入session。

4.3 session的delta改动
  • 条件3【else if (session.hasChanges()】,怎样判断session是否有改动?
    【重要】因为条件1和条件2触发的都是整个session对象的持久化,所以并没有真正涉及到SaveMode,SaveMode基本在这一小节有用到

首先看session hasChanges的判断:

boolean hasChanges() {
    return this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty();
}

可以看到当session的lastAccessTimeChanged为true或maxInactiveIntervalChanged为true或delta数据不为空,都被视作session有改动。

具体来看:

  • a. 什么时候session的lastAccessTime会改呢?即被调用过一次request.getSession()的时候。
    debug一下可以看到:

    image.png

  • b. maxInactiveIntervalChanged也比较好理解,即session本身是有过期时间的(如果设为负数即为永久有效),那么如果被触发过session.setMaxInactiveInterval(int interval),那么就会将maxInactiveIntervalChanged置为true。

public void setMaxInactiveInterval(Duration interval) {
    Assert.notNull(interval, "interval must not be null");
    this.delegate.setMaxInactiveInterval(interval);
    this.maxInactiveIntervalChanged = true;
    this.flushImmediateIfNecessary();
}
  • c delta数据不为空

【SaveMode的逻辑在这里!!!】
在HazelcastSession中声明了delta对象:

final class HazelcastSession implements Session {
        private Map<String, Object> delta = new HashMap();
    ...
}

那么什么时候delta会被put数据呢?

    1. HazelcastSession的构造函数,即:
  • isNew为true表示session刚被创建,那么毫无疑问会被加到delta中。
  • 另外当SaveMode=ALWAYS的时候,session中的所有attribute也会被加到delta中。
HazelcastSession(MapSession cached, boolean isNew) {     
    this.delegate = cached;
    this.isNew = isNew;
    this.originalId = cached.getId();
    if (this.isNew || Hazelcast4IndexedSessionRepository.this.saveMode == SaveMode.ALWAYS) {
        this.getAttributeNames().forEach((attributeName) -> {
            this.delta.put(attributeName, cached.getAttribute(attributeName));
         });
    }
}
    1. session.setAttribute(key, value)的时候,那么delta中也会放入相同的key。(默认SaveMode=ON_SET_ATTRIBUTE`,所以默认情况下即这个)。
    1. session.getAttribute(key)的时候,当SaveMode =ON_GET_ATTRIBUTE时**,也会把key放入delta中。

【总结】SaveMode有三个值:

  • ON_SET_ATTRIBUTE(默认),我们都知道session本质上也算是一个key, value的数据结构,这里的意思是当session.setAttribute(key, value)被调用的时候,我们会将这个key存入一个delta的map中,然后在session要被持久化的时候,只会做delta更新。在高并发的环境中,这个模式可以最小化因为并行的request导致的session值被串改。
    即:该模式下,我们手动调用session.setAttribute(key, value),这个value在持久化的节点肯定会被写回Hazelcast中。

  • ON_GET_ATTRIBUTE,即当调用session.getAttribute(key)时,就会把key,value放入delta的map中。即:该模式下,我们始终认为这个key所在的value被拿出后会被改动,所以需要放到delta中,以便在session被持久化的时候,这个value会被重新写回Hazelcast中。

  • ALWAYS:每次都是将session中所有的attributes都持久化回Hazelcast,比如session中持放着当前用户的信息,同时还存放着用户的购物车信息,那么即便当前request只是往购物车中加了一个商品,用户本身的信息也会被持久化到Hazelcast中。
    具体来说当session中有3个key,在最后response返回给浏览器的时候,这3个key所包含的value都会被再次提交到session中。
    该模式对并发量大的request并不友好,可能会增加session attributes被重写的风险。

5. 总结

FlushMode控制什么时候存储到Hazelcast中。
SaveMode控制存储对象的范围。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容