您好,登錄后才能下訂單哦!
這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)spring-session中的事件機(jī)制原理是什么,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
逐漸深入Spring-Session中的事件機(jī)制原理的探索。眾所周知,Servlet規(guī)范中有對HttpSession的事件的處理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,可以查看Package javax.servlet
在Spring-Session中也有相應(yīng)的Session事件機(jī)制實(shí)現(xiàn),包括Session創(chuàng)建/過期/刪除事件。
本文主要從以下方面探索Spring-Session中事件機(jī)制
Session事件的抽象
事件的觸發(fā)機(jī)制
Note:
這里的事件觸發(fā)機(jī)制只介紹基于RedissSession的實(shí)現(xiàn)?;趦?nèi)存Map實(shí)現(xiàn)的MapSession不支持Session事件機(jī)制。其他的Session實(shí)現(xiàn)這里也不做關(guān)注。
先來看下Session事件抽象UML類圖,整體掌握事件之間的依賴關(guān)系。
Session Event最頂層是ApplicationEvent,即Spring上下文事件對象。由此可以看出Spring-Session的事件機(jī)制是基于Spring上下文事件實(shí)現(xiàn)。
抽象的AbstractSessionEvent事件對象提供了獲取Session(這里的是指Spring Session的對象)和SessionId。
基于事件的類型,分類為:
Session創(chuàng)建事件
Session刪除事件
Session過期事件
Tips:
Session銷毀事件只是刪除和過期事件的統(tǒng)一,并無實(shí)際含義。
事件對象只是對事件本身的抽象,描述事件的屬性,如:
獲取事件產(chǎn)生的源:getSource獲取事件產(chǎn)生源
獲取相應(yīng)事件特性:getSession/getSessoinId獲取時(shí)間關(guān)聯(lián)的Session
下面再深入探索以上的Session事件是如何觸發(fā),從事件源到事件監(jiān)聽器的鏈路分析事件流轉(zhuǎn)過程。
閱讀本節(jié)前,讀者應(yīng)該了解Redis的Pub/Sub和KeySpace Notification,如果還不是很了解,傳送門Redis Keyspace Notifications和Pub/Sub。
上節(jié)中也介紹Session Event事件基于Spring的ApplicationEvent實(shí)現(xiàn)。先簡單認(rèn)識spring上下文事件機(jī)制:
ApplicationEventPublisher實(shí)現(xiàn)用于發(fā)布Spring上下文事件ApplicationEvent
ApplicationListener實(shí)現(xiàn)用于監(jiān)聽Spring上下文事件ApplicationEvent
ApplicationEvent抽象上下文事件
那么在Spring-Session中必然包含事件發(fā)布者ApplicationEventPublisher發(fā)布Session事件和ApplicationListener監(jiān)聽Session事件。
可以看出ApplicationEventPublisher發(fā)布一個(gè)事件:
@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用于監(jiān)聽相應(yīng)的事件:
@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:
這里使用到了發(fā)布/訂閱模式,事件監(jiān)聽器可以監(jiān)聽感興趣的事件,發(fā)布者可以發(fā)布各種事件。不過這是內(nèi)部的發(fā)布訂閱,即觀察者模式。
Session事件的流程實(shí)現(xiàn)如下:
上圖展示了Spring-Session事件流程圖,事件源來自于Redis鍵空間通知,在spring-data-redis項(xiàng)目中抽象MessageListener監(jiān)聽Redis事件源,然后將其傳播至spring應(yīng)用上下文發(fā)布者,由發(fā)布者發(fā)布事件。在spring上下文中的監(jiān)聽器Listener即可監(jiān)聽到Session事件。
因?yàn)閮烧呤荢pring框架提供的對Spring的ApplicationEvent的支持。Session Event基于ApplicationEvent實(shí)現(xiàn),必然也有其相應(yīng)發(fā)布者和監(jiān)聽器的的實(shí)現(xiàn)。
Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。所有關(guān)于RedisSession的管理操作都是由其實(shí)現(xiàn),所以Session的產(chǎn)生源是RedisOperationSessionRepository。
在RedisOperationSessionRepository中持有ApplicationEventPublisher對象用于發(fā)布Session事件。
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { @Override public void publishEvent(ApplicationEvent event) { } @Override public void publishEvent(Object event) { } };
但是該ApplicationEventPublisher是空實(shí)現(xiàn),實(shí)際實(shí)現(xiàn)是在應(yīng)用啟動(dòng)時(shí)由Spring-Session自動(dòng)配置。在spring-session-data-redis模塊中RedisHttpSessionConfiguration中有關(guān)于創(chuàng)建RedisOperationSessionRepository Bean時(shí)將調(diào)用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; } }
在進(jìn)行自動(dòng)配置時(shí),將上下文中的ApplicationEventPublisher的注入,實(shí)際上即ApplicationContext對象。
Note:
考慮篇幅原因,以上的RedisHttpSessionConfiguration至展示片段。
對于ApplicationListener是由應(yīng)用開發(fā)者自行實(shí)現(xiàn),注冊成Bean即可。當(dāng)有Session Event發(fā)布時(shí),即可監(jiān)聽。
/** * session事件監(jiān)聽器 * * @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事件的發(fā)布者和監(jiān)聽者,但是核心事件的觸發(fā)發(fā)布則是由Redis的鍵空間通知機(jī)制觸發(fā),當(dāng)有Session創(chuàng)建/刪除/過期時(shí),Redis鍵空間會(huì)通知Spring-Session應(yīng)用。
RedisOperationsSessionRepository實(shí)現(xiàn)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); }
該監(jiān)聽器即用來監(jiān)聽redis發(fā)布的消息。RedisOperationsSessionRepositorys實(shí)現(xiàn)了該Redis鍵空間消息通知監(jiān)聽器接口,實(shí)現(xiàn)如下:
public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener { @Override @SuppressWarnings("unchecked") public void onMessage(Message message, byte[] pattern) { // 獲取該消息發(fā)布的redis通道channel byte[] messageChannel = message.getChannel(); // 獲取消息體內(nèi)容 byte[] messageBody = message.getBody(); String channel = new String(messageChannel); // 如果是由Session創(chuàng)建通道發(fā)布的消息,則是Session創(chuàng)建事件 if (channel.startsWith(getSessionCreatedChannelPrefix())) { // 從消息體中載入Session Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer .deserialize(message.getBody()); // 發(fā)布創(chuàng)建事件 handleCreated(loaded, channel); return; } // 如果消息體不是以過期鍵前綴,直接返回。因?yàn)閟pring-session在redis中的key命名規(guī)則: // "${namespace}:sessions:expires:${sessionId}",如: // session.example:sessions:expires:a5236a19-7325-4783-b1f0-db9d4442db9a // 所以判斷過期或者刪除的鍵是否為spring-session的過期鍵。如果不是,可能是應(yīng)用中其他的鍵的操作,所以直接return String body = new String(messageBody); if (!body.startsWith(getExpiredKeyPrefix())) { return; } // 根據(jù)channel判斷鍵空間的事件類型del或者expire時(shí)間 boolean isDeleted = channel.endsWith(":del"); if (isDeleted || channel.endsWith(":expired")) { int beginIndex = body.lastIndexOf(":") + 1; int endIndex = body.length(); // Redis鍵空間消息通知內(nèi)容即操作的鍵,spring-session鍵中命名規(guī)則: // "${namespace}:sessions:expires:${sessionId}",以下是根據(jù)規(guī)則解析sessionId String sessionId = body.substring(beginIndex, endIndex); // 根據(jù)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); // 發(fā)布Session delete事件 if (isDeleted) { handleDeleted(session); } else { // 否則發(fā)布Session expire事件 handleExpired(session); } } } }
下續(xù)再深入每種事件產(chǎn)生的前世今生。
1.Session創(chuàng)建事件的觸發(fā)
由RedisOperationSessionRepository向Redis指定通道${namespace}:event:created:${sessionId}發(fā)布一個(gè)message
MessageListener的實(shí)現(xiàn)RedisOperationSessionRepository監(jiān)聽到Redis指定通道${namespace}:event:created:${sessionId}的消息
將其傳播至ApplicationEventPublisher
ApplicationEventPublisher發(fā)布SessionCreateEvent
ApplicationListener監(jiān)聽SessionCreateEvent,執(zhí)行相應(yīng)邏輯
RedisOperationSessionRepository中保存一個(gè)Session時(shí),判斷Session是否新創(chuàng)建。
如果新創(chuàng)建,則向
@Override public void save(RedisSession session) { session.saveDelta(); // 判斷是否為新創(chuàng)建的session if (session.isNew()) { // 獲取redis指定的channel:${namespace}:event:created:${sessionId}, // 如:session.example:event:created:82sdd-4123-o244-ps123 String sessionCreatedKey = getSessionCreatedChannel(session.getId()); // 向該通道發(fā)布session數(shù)據(jù) this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); // 設(shè)置session為非新創(chuàng)建 session.setNew(false); } }
該save方法的調(diào)用是由HttpServletResponse提交時(shí)——即返回客戶端響應(yīng)調(diào)用,上篇文章已經(jīng)詳解,這里不再贅述。關(guān)于RedisOperationSessionRepository實(shí)現(xiàn)MessageListener上述已經(jīng)介紹,這里同樣不再贅述。
Note:
這里有點(diǎn)繞。個(gè)人認(rèn)為RedisOperationSessionRepository發(fā)布創(chuàng)建然后再本身監(jiān)聽,主要是考慮分布式或者集群環(huán)境中SessionCreateEvent事件的處理。
2.Session刪除事件的觸發(fā)
Tips:
刪除事件中使用到了Redis KeySpace Notification,建議先了解該技術(shù)。
由RedisOperationSessionRepository刪除Redis鍵空間中的指定Session的過期鍵,Redis鍵空間會(huì)向**__keyevent@*:del**的channel發(fā)布刪除事件消息
MessageListener的實(shí)現(xiàn)RedisOperationSessionRepository監(jiān)聽到Redis指定通道**__keyevent@*:del**的消息
將其傳播至ApplicationEventPublisher
ApplicationEventPublisher發(fā)布SessionDeleteEvent
ApplicationListener監(jiān)聽SessionDeleteEvent,執(zhí)行相應(yīng)邏輯
當(dāng)調(diào)用HttpSession的invalidate方法讓Session失效時(shí),即會(huì)調(diào)用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(); // 調(diào)用刪除方法 SessionRepositoryFilter.this.sessionRepository.deleteById(getId()); } }
上篇中介紹了包裝Spring Session為HttpSession,這里不再贅述。這里重點(diǎn)分析deleteById內(nèi)容:
@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鍵空間產(chǎn)生del事件消息,被MessageListener即 // RedisOperationSessionRepository監(jiān)聽 this.sessionRedisOperations.delete(expireKey); session.setMaxInactiveInterval(Duration.ZERO); save(session); }
后續(xù)流程同SessionCreateEvent流程。
3.Session失效事件的觸發(fā)
Session的過期事件流程比較特殊,因?yàn)镽edis的鍵空間通知的特殊性,Redis鍵空間通知不能保證過期鍵的通知的及時(shí)性。
RedisOperationsSessionRepository中有個(gè)定時(shí)任務(wù)方法每整分運(yùn)行訪問整分Session過期鍵集合中的過期sessionId,如:spring:session:expirations:1439245080000。觸發(fā)Redis鍵空間會(huì)向**__keyevent@*:expired**的channel發(fā)布過期事件消息
MessageListener的實(shí)現(xiàn)RedisOperationSessionRepository監(jiān)聽到Redis指定通道**__keyevent@*:expired**的消息
將其傳播至ApplicationEventPublisher
ApplicationEventPublisher發(fā)布SessionDeleteEvent
ApplicationListener監(jiān)聽SessionDeleteEvent,執(zhí)行相應(yīng)邏輯
@Scheduled(cron = "0 * * * * *") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
定時(shí)任務(wù)每整分運(yùn)行,執(zhí)行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy實(shí)例,是RedisSession過期策略。
public void cleanExpiredSessions() { // 獲取當(dāng)前時(shí)間戳 long now = System.currentTimeMillis(); // 時(shí)間滾動(dòng)至整分,去掉秒和毫秒部分 long prevMin = roundDownMinute(now); if (logger.isDebugEnabled()) { logger.debug("Cleaning up sessions expiring at " + new Date(prevMin)); } // 根據(jù)整分時(shí)間獲取過期鍵集合,如:spring:session:expirations:1439245080000 String expirationKey = getExpirationKey(prevMin); // 獲取所有的所有的過期session Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 刪除過期Session鍵集合 this.redis.delete(expirationKey); // touch訪問所有已經(jīng)過期的session,觸發(fā)Redis鍵空間通知消息 for (Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); touch(sessionKey); } }
將時(shí)間戳滾動(dòng)至整分
static long roundDownMinute(long timeInMs) { Calendar date = Calendar.getInstance(); date.setTimeInMillis(timeInMs); // 清理時(shí)間錯(cuò)的秒位和毫秒位 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; }
調(diào)用Redis的Exists命令,訪問過期Session鍵,觸發(fā)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事件通知模塊就已經(jīng)很清晰:
Redis鍵空間Session事件源:Session創(chuàng)建通道/Session刪除通道/Session過期通道
Spring-Session中的RedisOperationsSessionRepository消息監(jiān)聽器監(jiān)聽Redis的事件類型
RedisOperationsSessionRepository負(fù)責(zé)將其傳播至ApplicationEventPublisher
ApplicationEventPublisher將其包裝成ApplicationEvent類型的Session Event發(fā)布
ApplicationListener監(jiān)聽Session Event,處理相應(yīng)邏輯
上述就是小編為大家分享的spring-session中的事件機(jī)制原理是什么了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。