《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!
“External: \thttp://{}:{}\n\t”+
“Doc: \thttp://{}:{}/doc.html\n\t”+
“----------------------------------------------------------”,
env.getProperty(“spring.application.name”),
env.getProperty(“server.port”),
host,port,
host,port);
}
在上面的代码中,我们指定Redis的命名空间是fish-admin:session,默认最大失效7200秒。
如果开发者默认不指定这两个属性的话,命名空间默认值是spring:session,默认最大时效则是1800秒
在上面我们已经说过了,既然是看源码,我们需要找到入口,这是看源码最好的方式,我们在使用Spring Session组件时,需要使用@EnableRedisHttpSession注解,那么该注解就是我们需要重点关注的对象,我们需要搞清楚,该注解的作用是什么?
EnableRedisHttpSession.java部分源码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
//more property…
}
在该注解中,我们可以看到,最关键的是在该注解之上,使用@Import注解导入了RedisHttpSessionConfiguration.java配置类,如果你经常翻看Spring Boot相关的源码,你会敏锐地察觉到,该配置类就是我们最终要找的类
先来看该类的UML图,如下:
该类实现了Spring框架中很多Aware类型接口,Aware类型的接口我们都知道,Spring容器在启动创建实体Bean后,会调用Aware系列的set方法传参赋值
当然,最核心的,我们从源码中可以看到,是Spring Session组件会向Spring容器中注入两个实体Bean,代码如下:
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + “*”)));
return container;
}
RedisIndexedSessionRepository以及RedisMessageListenerContainer的实体Bean
-
RedisMessageListenerContainer:该类是Redis的消息通知回调机制实体类,Redis提供了针对不同Key的操作回调消息通知,比如常见的删除key、key过期等事件的回调,在Spring Session组件中注入该实体Bean,从代码中也可以看出是用来监听处理Session会话的过期以及删除事件
-
RedisIndexedSessionRepository:该类是Spring Session组件提供基于Redis的针对Session会话一系列操作的具体实现类,是我们接下来源码分析的重点。
先来看RedisIndexedSessionRepository类的UML类图结构,如下图:
RedisIndexedSessionRepository实现了FindByIndexNameSessionRepository接口,而FindByIndexNameSessionRepository接口又继承Spring Security权限框架提供的顶级SessionRepository接口,UML类图中,我们可以得到几个重要的信息:
-
RedisIndexedSessionRepository拥有创建Session会话、销毁删除Session会话的能力
-
RedisIndexedSessionRepository由于实现自FindByIndexNameSessionRepository接口,而该接口提供了根据PrincipalName查找Session会话的能力
-
拥有Redis回调事件的处理消息能力,因为实现了MessageListener接口
SessionRepository是Spring Security提供的顶级接口,源码如下:
public interface SessionRepository {
/**
-
Creates a new {@link Session} that is capable of being persisted by this
-
{@link SessionRepository}.
-
-
This allows optimizations and customizations in how the {@link Session} is
-
persisted. For example, the implementation returned might keep track of the changes
-
ensuring that only the delta needs to be persisted on a save.
-
@return a new {@link Session} that is capable of being persisted by this
-
{@link SessionRepository}
*/
S createSession();
/**
-
Ensures the {@link Session} created by
-
{@link org.springframework.session.SessionRepository#createSession()} is saved.
-
-
Some implementations may choose to save as the {@link Session} is updated by
-
returning a {@link Session} that immediately persists any changes. In this case,
-
this method may not actually do anything.
-
@param session the {@link Session} to save
*/
void save(S session);
/**
-
Gets the {@link Session} by the {@link Session#getId()} or null if no
-
{@link Session} is found.
-
@param id the {@link org.springframework.session.Session#getId()} to lookup
-
@return the {@link Session} by the {@link Session#getId()} or null if no
-
{@link Session} is found.
*/
S findById(String id);
/**
-
Deletes the {@link Session} with the given {@link Session#getId()} or does nothing
-
if the {@link Session} is not found.
-
@param id the {@link org.springframework.session.Session#getId()} to delete
*/
void deleteById(String id);
}
该接口提供四个方法:
-
createSession:创建Session会话
-
save:保存Session会话
-
findById:根据SessionId查找获取Session会话对象信息
-
deleteById:根据SessionId进行删除
FindByIndexNameSessionRepository源码主要是提供根据账号名称进行查询的功能,如下:
public interface FindByIndexNameSessionRepository extends SessionRepository {
/**
- 当前存储的用户名前缀,使用Redis进行存储时,存储的key值是:redisNamespace+
*/
String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
.concat(“.PRINCIPAL_NAME_INDEX_NAME”);
/**
-
Find a {@link Map} of the session id to the {@link Session} of all sessions that
-
contain the specified index name index value.
-
@param indexName the name of the index (i.e.
-
{@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})
-
@param indexValue the value of the index to search for.
-
@return a {@code Map} (never {@code null}) of the session id to the {@code Session}
-
of all sessions that contain the specified index name and index value. If no
-
results are found, an empty {@code Map} is returned.
*/
Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
/**
-
Find a {@link Map} of the session id to the {@link Session} of all sessions that
-
contain the index with the name
-
{@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} and the
-
specified principal name.
-
@param principalName the principal name
-
@return a {@code Map} (never {@code null}) of the session id to the {@code Session}
-
of all sessions that contain the specified principal name. If no results are found,
-
an empty {@code Map} is returned.
-
@since 2.1.0
*/
default Map<String, S> findByPrincipalName(String principalName) {
return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
}
}
该接口最核心的功能是提供了根据用户名查找获取Session会话的接口,这对我们后面实现踢人功能很有帮助。
通过查看SessionRepository接口以及FindByIndexNameSessionRepository接口的源码我们得知:
-
Redis的实现最终实现了这两个接口,因此获得了基于Redis中间件创建及销毁Session会话的能力
-
根据账号去查找当前的所有登录会话Session符合我们最终需要服务端主动踢人下线的功能需求。
接下来我们只需要关注RedisIndexedSessionRepository的实现即可。首先来看findByPrincipalName方法,源码如下:
@Override
public Map<String, RedisSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
//如果名称不匹配,则直接反馈空集合Map
if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
//获取拼装的Key值
String principalKey = getPrincipalKey(indexValue);
//从Redis中获取该Key值的成员数
Set sessionIds = this.sessionRedisOperations.boundSetOps(principalKey).members();
//初始化Map集合
Map<String, RedisSession> sessions = new HashMap<>(sessionIds.size());
//循环遍历
for (Object id : sessionIds) {
//根据id查找Session会话
RedisSession session = findById((String) id);
if (session != null) {
sessions.put(session.getId(), session);
}
}
return sessions;
}
String getPrincipalKey(String principalName) {
return this.namespace + “index:” + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + “:”
- principalName;
}
接下来我们看删除Session会话的方法实现:
@Override
public void deleteById(String sessionId) {
//根据sessionId获取Session会话
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
//从Redis中移除所有存储的针对principal的key值
cleanupPrincipalIndex(session);
//Redis中删除SessionId所对应的key值
this.expirationPolicy.onDelete(session);
//移除Session会话创建时,存储的过期key值
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
//设置当前session会话最大存活时间为0
session.setMaxInactiveInterval(Duration.ZERO);
//执行save方法
save(session);
}
从上面的代码中,我们已经知道了Spring Session组件对于Session相关的处理方法,其实我们基于上面的两个核心方法,我们已经获得了踢人下线的能力,但是,既然RedisIndexedSessionRepository实现了MessageListener接口,我们需要继续跟踪一下该接口的具体实现方法,我们直接来看onMessage方法,代码如下:
@Override
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?
@SuppressWarnings(“unchecked”)
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);
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) {
handleDeleted(session);
}
else {
handleExpired(session);
}
}
}
private void handleDeleted(RedisSession session) {
publishEvent(new SessionDeletedEvent(this, session));
}
private void handleExpired(RedisSession session) {
publishEvent(new SessionExpiredEvent(this, session));
}
private void publishEvent(ApplicationEvent event) {
try {
this.eventPublisher.publishEvent(event);
}
catch (Throwable ex) {
logger.error("Error publishing " + event + “.”, ex);
}
}
在onMessage方法中,最核心的是最后一个判断,分别执行handleDeleted和handleExpired方法,从源码中我们可以看到,当当前Session会话被删除或者失效时,Spring Session会通过ApplicationEventPublisher广播一个事件,分别处理SessionExpiredEvent和SessionDeletedEvent事件
这是Spring Session组件为开发者预留的针对Session会话的Event事件,如果开发者对于当前的Sesssion会话的删除或者失效有特殊的处理需求,则可以通过监听该事件进行处理。
例如,开发者针对Session会话的操作都需要做业务操作,记录日志保存到DB数据库中,此时,开发者只需要使用Spring提供的EventListener实现就可以很轻松的实现,示例代码如下:
@Component
public class SecuritySessionEventListener {
@EventListener
public void sessionDestroyed(SessionDestroyedEvent event) {
//session销毁事件处理方法…
}
@EventListener
public void sessionCreated(SessionCreatedEvent event) {
//session创建会话事件处理方法…
}
@EventListener
public void sessionExired(SessionExpiredEvent event) {
//session会话过期事件处理方法…
}
}
4.解决方案
======
我们分析了Spring Session针对Session基于Redis的实现,接下来,我们从源码中已经知道了该如何查找Session会话以及销毁会话的方法,此时,我们可以来改造我们的框架代码了
创建SessionService接口,代码如下:
public interface SessionService {
/**
-
@param account
-
@return
*/
boolean hasLogin(String account);
/**
-
根據账号查找当前session会话
-
@param account 账号
-
@return
*/
Map<String, ? extends Session> loadByAccount(String account);
/**
-
销毁当前session会话
-
@param account
*/
void destroySession(String account);
}
声明该接口主要包含3个方法:
总结
以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!
下面给大家分享下我的面试大全资料
- 第一份是我的后端JAVA面试大全
后端JAVA面试大全
- 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
- 第三份是Spring全家桶资料
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!
-
@param account 账号
-
@return
*/
Map<String, ? extends Session> loadByAccount(String account);
/**
-
销毁当前session会话
-
@param account
*/
void destroySession(String account);
}
声明该接口主要包含3个方法:
总结
以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!
下面给大家分享下我的面试大全资料
- 第一份是我的后端JAVA面试大全
[外链图片转存中…(img-avQg5Cos-1714752305690)]
后端JAVA面试大全
- 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理
[外链图片转存中…(img-bjYYq0Oy-1714752305691)]
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
- 第三份是Spring全家桶资料
[外链图片转存中…(img-i84kqdVj-1714752305692)]
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门,即可获取!