1. 程式人生 > 實用技巧 >spring-session原始碼解析

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

https://github.com/spring-projects/spring-session/issues/93

https://www.cnblogs.com/lxyit/p/9672097.html