spring-session原始碼解析
目錄
目的
學習一下spring-session中包含的一些設計,理解其設計思想,其次是瞭解內部原始碼,邏輯。
工程結構
來自spring-session的思考
首先思考一下spring-session要解決什麼問題,其次達到什麼樣的設計要求,
我們首先來正向推導,然後在結合程式碼逆向推導,他達到了一些什麼要求
基本要求
- 原業務無感知(重要)
- 支援多種儲存介質
- 支援多種servlet容器(重要)
- 效能
- 穩定性、可靠性
要想做到第1、3條,基本限定必須要基於標準servlet協議
基礎知識
HttpSession (javax.servlet.http) 介面
Session (Spring-Session 介面) MapSession RedisSession JdbcSession
ServletRequest->HttpServletRequest (javax.servlet;)
ServletRequestWrapper(類)->HttpServletRequestWrapper(類)(javax.servlet;)
SessionRepository(Spring Session介面) 一個管理Session例項的倉庫
SessionRepositoryFilter(將spring-session 裡面的Session轉換 Httpsession 的實現)
SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper (spring-session)
2種設計模式
- 介面卡模式
- 包裝者模式
思考 為什麼要用介面卡模式?spring為什麼要另起一個Session的介面
1、通過調整HttpSessionAdapter 就可以遮蔽兩種介面之間的差異
2、不僅僅能支援和滿足servlet規範,還能方便擴充套件其他規範
3、通過介面卡修改 即可適應不同的規範,sevlet2?
關鍵類
spring-session-core | spring-session-data-redis |
---|---|
SessionRepositoryFilter | |
Session | RedisSession RedisOperationsSessionRepositoryn內部類 |
SessionRepository | RedisOperationsSessionRepository |
SpringHttpSessionConfiguration | RedisHttpSessionConfiguration |
各個主要配置類作用
@EnableRedisHttpSession位於spring-session-data-redis module 中
並@Import RedisHttpSessionConfiguration.class
RedisHttpSessionConfiguration繼承spring-session-core中的SpringHttpSessionConfiguration
其中SpringHttpSessionConfiguration只關注filter,cookie解析,sessionId解析
RedisHttpSessionConfiguration 主要作用構建SessionRepository (建立redis 序列化, redis連線工廠,名稱空間,快取有效期)
redisMessageListenerContainer 快取的一些監聽器
整體架構
獲取session
SessionRepositoryRequestWrapper getSession
@Override
public HttpSessionWrapper getSession(boolean create) {
//先獲取request上下文中的,為什麼,因為一次請求可能在業務層已經多次獲取了
//先放在本地request的ConcurrentHashMap中,不必每次去redis取
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//假如請求已經返回,第二次來請求,就獲取當前request的sessionId
//從sessionRepository 拿出session
// 兩種情況,一是拿到了,二是沒拿到
// 拿到了 就把放入當前request的ConcurrentHashMap
// 沒拿到,說明session過期,或者非法的sessionId
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.setNew(false);
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. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ 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;
}
session建立
RedisOperationSessionsRepository.java
public RedisSession createSession() {
Duration maxInactiveInterval = Duration
.ofSeconds((this.defaultMaxInactiveInterval != null)
? this.defaultMaxInactiveInterval
: MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
RedisSession session = new RedisSession(maxInactiveInterval);
//看配置是否立即提交到session
session.flushImmediateIfNecessary();
return session;
}
session建立&更新(提交)
session提交一共幹了如下幾件事
* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
* EXPIRE spring:session:expirations1439245080000 2100
RedisOperationsSessionRepository.java
private void save() {
//servlet3.1規範,防止會話固定攻擊
saveChangeSessionId();
saveDelta();
}
private void saveChangeSessionId() {
String sessionId = getId();
//判斷是否變換了sessionId
if (sessionId.equals(this.originalSessionId)) {
return;
}
//並且不是新的session,需要更改原來sessionId的值
if (!isNew()) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(sessionId);
//更改主session的key值
try {
RedisOperationsSessionRepository.this.sessionRedisOperations
.rename(originalSessionIdKey, sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
try {
//更改過期session鍵 sessionId的值
RedisOperationsSessionRepository.this.sessionRedisOperations
.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
this.originalSessionId = sessionId;
}
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
//持久化session屬性
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(
SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey)
|| this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(
this.originalPrincipalName);
RedisOperationsSessionRepository.this.sessionRedisOperations
.boundSetOps(originalPrincipalRedisKey).remove(sessionId);
}
String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisOperationsSessionRepository.this.sessionRedisOperations
.boundSetOps(principalRedisKey).add(sessionId);
}
}
this.delta = new HashMap<>(this.delta.size());
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval())
.toEpochMilli()
: null;
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
}
public void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
String keyToExpire = "expires:" + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
//如果不為空,說明是老session,需更新過期時間?如何更新,刪除set裡面的值
if (originalExpirationTimeInMilli != null) {
long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
if (toExpire != originalRoundedUp) {
String expireKey = getExpirationKey(originalRoundedUp);
this.redis.boundSetOps(expireKey).remove(keyToExpire);
}
}
long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();
String sessionKey = getSessionKey(keyToExpire);
//永不過期的session
if (sessionExpireInSeconds < 0) {
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).persist();
this.redis.boundHashOps(getSessionKey(session.getId())).persist();
return;
}
String expireKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis
.boundSetOps(expireKey);
expireOperations.add(keyToExpire);
long fiveMinutesAfterExpires = sessionExpireInSeconds
+ TimeUnit.MINUTES.toSeconds(5);
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
if (sessionExpireInSeconds == 0) {
this.redis.delete(sessionKey);
}
else {
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
TimeUnit.SECONDS);
}
this.redis.boundHashOps(getSessionKey(session.getId()))
.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}
為何要這樣設計呢
假設一下
- 解決過期Session不能被及時清除的問題 (定時任務每隔一個鐘去訪問redis,觸發清除)
- 為了不遍歷全空間資料,將一分鐘過期的資料放到同一個set下面,每分鐘的定時任務只去清除這個set下的資料
- 即使資料過期,也不要立即刪除當前,還有過期的事件處理
session過期
需要監聽session過期事件,並且進行觸發
RedisHttpSessionConfiguration.java
//定時任務掃描
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(),
this.cleanupCron);
}
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);
if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
}
//獲取過期key
//spring:session:expirations:1439245080000
String expirationKey = getExpirationKey(prevMin);
//expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
this.redis.delete(expirationKey);
//分別touch過期key
for (Object session : sessionsToExpire) {
// spring:session:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
String sessionKey = getSessionKey((String) session);
//觸發刪除,過期事件
touch(sessionKey);
}
}
RedisOperationsSessionRepository.java
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
String channel = new String(messageChannel);
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
// TODO: is this thread safe?
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer
.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
//還是能取到session的 因為過期時間晚了5分鐘,而且刪除的是
//spring:session:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session "
+ sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
cleanupPrincipalIndex(session);
if (isDeleted) {
// 給一些session事件監聽器處理
handleDeleted(session);
}
else {
handleExpired(session);
}
}
}
private void cleanupPrincipalIndex(RedisSession session) {
String sessionId = session.getId();
String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(session);
if (principal != null) {
this.sessionRedisOperations.boundSetOps(getPrincipalKey(principal))
.remove(sessionId);
}
}
private void handleCreated(Map<Object, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
publishEvent(new SessionCreatedEvent(this, session));
}
參考文獻
https://github.com/spring-projects/spring-session/issues/92