上一篇文章中介紹了Spring-Session的核心原理,Filter,Session,Repository等等,傳送門:spring-session(一)揭祕。html
這篇繼上一篇的原理逐漸深刻Spring-Session中的事件機制原理的探索。衆所周知,Servlet規範中有對HttpSession的事件的處理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,能夠查看Package javax.servletjava
在Spring-Session中也有相應的Session事件機制實現,包括Session建立/過時/刪除事件。git
本文主要從如下方面探索Spring-Session中事件機制github
Note:
這裏的事件觸發機制只介紹基於RedissSession的實現。基於內存Map實現的MapSession不支持Session事件機制。其餘的Session實現這裏也不作關注。web
先來看下Session事件抽象UML類圖,總體掌握事件之間的依賴關係。redis
Session Event最頂層是ApplicationEvent,即Spring上下文事件對象。由此能夠看出Spring-Session的事件機制是基於Spring上下文事件實現。spring
抽象的AbstractSessionEvent事件對象提供了獲取Session(這裏的是指Spring Session的對象)和SessionId。api
基於事件的類型,分類爲:session
Tips:
Session銷燬事件只是刪除和過時事件的統一,並沒有實際含義。oracle
事件對象只是對事件自己的抽象,描述事件的屬性,如:
下面再深刻探索以上的Session事件是如何觸發,從事件源到事件監聽器的鏈路分析事件流轉過程。
閱讀本節前,讀者應該瞭解Redis的Pub/Sub和KeySpace Notification,若是還不是很瞭解,傳送門Redis Keyspace Notifications和Pub/Sub。
上節中也介紹Session Event事件基於Spring的ApplicationEvent實現。先簡單認識spring上下文事件機制:
那麼在Spring-Session中必然包含事件發佈者ApplicationEventPublisher發佈Session事件和ApplicationListener監聽Session事件。
能夠看出ApplicationEventPublisher發佈一個事件:
@FunctionalInterface public interface ApplicationEventPublisher { /** * Notify all <strong>matching</strong> listeners registered with this * application of an application event. Events may be framework events * (such as RequestHandledEvent) or application-specific events. * @param event the event to publish * @see org.springframework.web.context.support.RequestHandledEvent */ default void publishEvent(ApplicationEvent event) { publishEvent((Object) event); } /** * Notify all <strong>matching</strong> listeners registered with this * application of an event. * <p>If the specified {@code event} is not an {@link ApplicationEvent}, * it is wrapped in a {@link PayloadApplicationEvent}. * @param event the event to publish * @since 4.2 * @see PayloadApplicationEvent */ void publishEvent(Object event); }
ApplicationListener用於監聽相應的事件:
@FunctionalInterface public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /** * Handle an application event. * @param event the event to respond to */ void onApplicationEvent(E event); }
Tips:
這裏使用到了發佈/訂閱模式,事件監聽器能夠監聽感興趣的事件,發佈者能夠發佈各類事件。不過這是內部的發佈訂閱,即觀察者模式。
Session事件的流程實現以下:
上圖展現了Spring-Session事件流程圖,事件源來自於Redis鍵空間通知,在spring-data-redis項目中抽象MessageListener監聽Redis事件源,而後將其傳播至spring應用上下文發佈者,由發佈者發佈事件。在spring上下文中的監聽器Listener便可監聽到Session事件。
由於二者是Spring框架提供的對Spring的ApplicationEvent的支持。Session Event基於ApplicationEvent實現,必然也有其相應發佈者和監聽器的的實現。
Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。全部關於RedisSession的管理操做都是由其實現,因此Session的產生源是RedisOperationSessionRepository。
在RedisOperationSessionRepository中持有ApplicationEventPublisher對象用於發佈Session事件。
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { @Override public void publishEvent(ApplicationEvent event) { } @Override public void publishEvent(Object event) { } };
可是該ApplicationEventPublisher是空實現,實際實現是在應用啓動時由Spring-Session自動配置。在spring-session-data-redis模塊中RedisHttpSessionConfiguration中有關於建立RedisOperationSessionRepository Bean時將調用set方法將ApplicationEventPublisher配置。
@Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer { private ApplicationEventPublisher applicationEventPublisher; @Bean public RedisOperationsSessionRepository sessionRepository() { RedisTemplate<Object, Object> redisTemplate = createRedisTemplate(); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository( redisTemplate); // 注入依賴 sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); if (this.defaultRedisSerializer != null) { sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); } sessionRepository .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); if (StringUtils.hasText(this.redisNamespace)) { sessionRepository.setRedisKeyNamespace(this.redisNamespace); } sessionRepository.setRedisFlushMode(this.redisFlushMode); return sessionRepository; } // 注入上下文中的ApplicationEventPublisher Bean @Autowired public void setApplicationEventPublisher( ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } }
在進行自動配置時,將上下文中的ApplicationEventPublisher的注入,實際上即ApplicationContext對象。
Note:
考慮篇幅緣由,以上的RedisHttpSessionConfiguration至展現片斷。
對於ApplicationListener是由應用開發者自行實現,註冊成Bean便可。當有Session Event發佈時,便可監聽。
/** * session事件監聽器 * * @author huaijin */ @Component public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> { private static final String CURRENT_USER = "currentUser"; @Override public void onApplicationEvent(SessionDeletedEvent event) { Session session = event.getSession(); UserVo userVo = session.getAttribute(CURRENT_USER); System.out.println("Current session's user:" + userVo.toString()); } }
以上部分探索了Session事件的發佈者和監聽者,可是核心事件的觸發發佈則是由Redis的鍵空間通知機制觸發,當有Session建立/刪除/過時時,Redis鍵空間會通知Spring-Session應用。
RedisOperationsSessionRepository實現spring-data-redis中的MessageListener接口。
/** * Listener of messages published in Redis. * * @author Costin Leau * @author Christoph Strobl */ public interface MessageListener { /** * Callback for processing received objects through Redis. * * @param message message must not be {@literal null}. * @param pattern pattern matching the channel (if specified) - can be {@literal null}. */ void onMessage(Message message, @Nullable byte[] pattern); }
該監聽器即用來監聽redis發佈的消息。RedisOperationsSessionRepositorys實現了該Redis鍵空間消息通知監聽器接口,實現以下:
public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener { @Override @SuppressWarnings("unchecked") public void onMessage(Message message, byte[] pattern) { // 獲取該消息發佈的redis通道channel byte[] messageChannel = message.getChannel(); // 獲取消息體內容 byte[] messageBody = message.getBody(); String channel = new String(messageChannel); // 若是是由Session建立通道發佈的消息,則是Session建立事件 if (channel.startsWith(getSessionCreatedChannelPrefix())) { // 從消息體中載入Session Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer .deserialize(message.getBody()); // 發佈建立事件 handleCreated(loaded, channel); return; } // 若是消息體不是以過時鍵前綴,直接返回。由於spring-session在redis中的key命名規則: // "${namespace}:sessions:expires:${sessionId}",如: // session.example:sessions:expires:a5236a19-7325-4783-b1f0-db9d4442db9a // 因此判斷過時或者刪除的鍵是否爲spring-session的過時鍵。若是不是,多是應用中其餘的鍵的操做,因此直接return String body = new String(messageBody); if (!body.startsWith(getExpiredKeyPrefix())) { return; } // 根據channel判斷鍵空間的事件類型del或者expire時間 boolean isDeleted = channel.endsWith(":del"); if (isDeleted || channel.endsWith(":expired")) { int beginIndex = body.lastIndexOf(":") + 1; int endIndex = body.length(); // Redis鍵空間消息通知內容即操做的鍵,spring-session鍵中命名規則: // "${namespace}:sessions:expires:${sessionId}",如下是根據規則解析sessionId String sessionId = body.substring(beginIndex, endIndex); // 根據sessionId加載session 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); // 發佈Session delete事件 if (isDeleted) { handleDeleted(session); } else { // 不然發佈Session expire事件 handleExpired(session); } } } }
下續再深刻每種事件產生的前世此生。
1.Session建立事件的觸發
RedisOperationSessionRepository中保存一個Session時,判斷Session是否新建立。
若是新建立,則向
@Override public void save(RedisSession session) { session.saveDelta(); // 判斷是否爲新建立的session if (session.isNew()) { // 獲取redis指定的channel:${namespace}:event:created:${sessionId}, // 如:session.example:event:created:82sdd-4123-o244-ps123 String sessionCreatedKey = getSessionCreatedChannel(session.getId()); // 向該通道發佈session數據 this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); // 設置session爲非新建立 session.setNew(false); } }
該save方法的調用是由HttpServletResponse提交時——即返回客戶端響應調用,上篇文章已經詳解,這裏再也不贅述。關於RedisOperationSessionRepository實現MessageListener上述已經介紹,這裏一樣再也不贅述。
Note:
這裏有點繞。我的認爲RedisOperationSessionRepository發佈建立而後再自己監聽,主要是考慮分佈式或者集羣環境中SessionCreateEvent事件的處理。
2.Session刪除事件的觸發
Tips:
刪除事件中使用到了Redis KeySpace Notification,建議先了解該技術。
當調用HttpSession的invalidate方法讓Session失效時,即會調用RedisOperationSessionRepository的deleteById方法刪除Session的過時鍵。
/** * Allows creating an HttpSession from a Session instance. * * @author Rob Winch * @since 1.0 */ private final class HttpSessionWrapper extends HttpSessionAdapter<S> { HttpSessionWrapper(S session, ServletContext servletContext) { super(session, servletContext); } @Override public void invalidate() { super.invalidate(); SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true; setCurrentSession(null); clearRequestedSessionCache(); // 調用刪除方法 SessionRepositoryFilter.this.sessionRepository.deleteById(getId()); } }
上篇中介紹了包裝Spring Session爲HttpSession,這裏再也不贅述。這裏重點分析deleteById內容:
@Override public void deleteById(String sessionId) { // 若是session爲空則返回 RedisSession session = getSession(sessionId, true); if (session == null) { return; } cleanupPrincipalIndex(session); this.expirationPolicy.onDelete(session); // 獲取session的過時鍵 String expireKey = getExpiredKey(session.getId()); // 刪除過時鍵,redis鍵空間產生del事件消息,被MessageListener即 // RedisOperationSessionRepository監聽 this.sessionRedisOperations.delete(expireKey); session.setMaxInactiveInterval(Duration.ZERO); save(session); }
後續流程同SessionCreateEvent流程。
3.Session失效事件的觸發
Session的過時事件流程比較特殊,由於Redis的鍵空間通知的特殊性,Redis鍵空間通知不能保證過時鍵的通知的及時性。
@Scheduled(cron = "0 * * * * *") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
定時任務每整分運行,執行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy實例,是RedisSession過時策略。
public void cleanExpiredSessions() { // 獲取當前時間戳 long now = System.currentTimeMillis(); // 時間滾動至整分,去掉秒和毫秒部分 long prevMin = roundDownMinute(now); if (logger.isDebugEnabled()) { logger.debug("Cleaning up sessions expiring at " + new Date(prevMin)); } // 根據整分時間獲取過時鍵集合,如:spring:session:expirations:1439245080000 String expirationKey = getExpirationKey(prevMin); // 獲取全部的全部的過時session Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 刪除過時Session鍵集合 this.redis.delete(expirationKey); // touch訪問全部已通過期的session,觸發Redis鍵空間通知消息 for (Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); touch(sessionKey); } }
將時間戳滾動至整分
static long roundDownMinute(long timeInMs) { Calendar date = Calendar.getInstance(); date.setTimeInMillis(timeInMs); // 清理時間錯的秒位和毫秒位 date.clear(Calendar.SECOND); date.clear(Calendar.MILLISECOND); return date.getTimeInMillis(); }
獲取過時Session的集合
String getExpirationKey(long expires) { return this.redisSession.getExpirationsKey(expires); } // 如:spring:session:expirations:1439245080000 String getExpirationsKey(long expiration) { return this.keyPrefix + "expirations:" + expiration; }
調用Redis的Exists命令,訪問過時Session鍵,觸發Redis鍵空間消息
/** * By trying to access the session we only trigger a deletion if it the TTL is * expired. This is done to handle * https://github.com/spring-projects/spring-session/issues/93 * * @param key the key */ private void touch(String key) { this.redis.hasKey(key); }
至此Spring-Session的Session事件通知模塊就已經很清晰: