【前置文章】
- https://www.jianshu.com/p/cb2b01dcde9c
- 本文源码基于spring-session-hazelcalst 2.7.0
1. Spring Session是如何工作的?
1.1 创建了Bean: SessionRepository
Source code: github
本章节重要的类如下:
【以下是装配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
。
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中介绍的HazelcastIndexedSessionRepository
的save(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数据呢?
- 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));
});
}
}
- session.setAttribute(key, value)的时候,那么delta中也会放入相同的key。(默认SaveMode=ON_SET_ATTRIBUTE`,所以默认情况下即这个)。
- session.getAttribute(key)的时候,当
SaveMode =ON_GET_ATTRIBUTE
时**,也会把key放入delta中。
- session.getAttribute(key)的时候,当
【总结】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控制存储对象的范围。