spring-session中对于servlet3.0标准中
HttpSessionAttributeListener
监听器没有支持,所以如果想要监听spring-session中session属性的变化就需要自己编码处理,经上网查阅资料与源码,问题得以解决。
起因
- 最近的项目需要统计登录session,但是现有的系统中,用户访问后创建的session不一定是登录的session,只有在session的属性中设置了指定的值才算作登录。servlet3.0中支持
HttpSessionAttributeListener
用来监听session属性的修改,但是蛋疼的是spring-session中对于servlet标准监听器只支持HttpSessionListener
,所以得自己想办法解决。
问题剖析
- spring-session的GitHub repository上有人提了issue,spring-session目前还不支持
HttpSessionAttributeListener
,通过网上对于spring-session源码解析的资料,得知spring-session对于HttpSessionListener
的处理是通过redis
的Redis Keyspace Notifications功能实现的。为此,我们可以参考HttpSessionListener
的实现逻辑,处理下session属性的监听。 spring-session在创建session后,会持久化session到redis中,对于一个session,会创建3个redis的值,分别为:
spring:session:sessions:+sessionId
:此值为hash,其中存储了session中的属性,还包括session的持续时长、上次访问时间以及创建时间spring:session:sessions:expires:+sessionId
:此key中没有值,主要是利用这个key的过期时间,确定session是否过期spring:session:expirations:+时间戳
:此值为set,其中存储了对应时间过期的sessionid,通过源码可以得知,此处所有的时间戳都是整分钟的时间戳,spring-session在处理时会先将session的过期时间取下一个最近的整分钟时间戳,并将sessionId存储到对应的set中。
spring-session中在操作
redis
时,主要是通过RedisOperationsSessionRepository
这个类实现的,我们可以在业务代码中注入此类的对象,即可操作session。RedisOperationsSessionRepository
中涉及到保存session到redis
的方法是 save() 方法,代码如下:1
2
3
4
5
6
7
8public void save(RedisSession session) {
session.saveDelta(); // 保存session到redis
if (session.isNew()) { // session是新建的,需要通过pub/sub发布消息,触发httpSessionListener,不在本篇文章分析的范围内
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}其中最主要的就是
session.saveDelta()
方法,RedisSession
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()}
* is invoked all the attributes that have been changed will be persisted.
*
* @author Rob Winch
* @since 1.0
*/
final class RedisSession implements Session {
private final MapSession cached; // 内存中的session
private Instant originalLastAccessTime; // 上次访问时间
private Map<String, Object> delta = new HashMap<>(); // 更新的数据
private boolean isNew;// 是否发布过创建的消息
private String originalPrincipalName;
/**
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
*/
RedisSession() {
this(new MapSession());
// 新建session的时候会默认将创建时间、上次访问时间和session有效期添加到session
this.delta.put(CREATION_TIME_ATTR, getCreationTime().toEpochMilli());
this.delta.put(MAX_INACTIVE_ATTR, (int) getMaxInactiveInterval().getSeconds());
this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime().toEpochMilli());
this.isNew = true;
this.flushImmediateIfNecessary();
}
...
省略无关代码
...
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
// 保存session到redis的方法
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
// 此处为保存session属性,delta为更新的session信息
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
...
省略无关代码
...
this.delta = new HashMap<>(this.delta.size());
// 下面是更新过期时间的逻辑
Long originalExpiration = this.originalLastAccessTime == null ? null
: this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli();
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
}
}getSessionBoundHashOperations
方法代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14private String keyPrefix = DEFAULT_SPRING_SESSION_REDIS_PREFIX;
static final String DEFAULT_SPRING_SESSION_REDIS_PREFIX = "spring:session:";
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(
String sessionId) {
// 此处拼装得到的就是spring:session:sessions:sessionId
String key = getSessionKey(sessionId);
// 操作redis中session对应的set,更新hashkey\hashvalue
return this.sessionRedisOperations.boundHashOps(key);
}
String getSessionKey(String sessionId) {
return this.keyPrefix + "sessions:" + sessionId;
}可以看到,最终session属性变化的时候,会从
RedisSession
的cache的MapSession
中将属性持久化到redis对应的set中。redis
的Redis Keyspace Notifications,官网文档指出,此功能可以在redis的数据发生变化时,通过 Pub/Sub 将变动发布到对应的频道,可以用来监控数据的变化,例如- 订阅频道:
__keyspace@0__:mykey
可以监控db0上的key为mykey
的数据变化; - 订阅频道:
__keyspace@0__:del
可以监控db0上的所有del命令操作的数据;
如此一来便可以通过
Keyspace Notifications
订阅频道,获取session中属性的变化- 订阅频道:
解决方案
整体的思路就是监听spring-session对应的
redis
的db的hset
、hdel
操作,当检测到向session中设置值时,即可触发对应的类,并调用需要调用的方法即可。以监听hset
为例,具体代码如下:在配置文件中增加redis监听配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!-- 定义Spring Redis的序列化器 -->
<!-- String序列化 -->
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
<!-- 将监听实现类注册到spring容器中 -->
<bean id="dataSyncEventListener" class="com.yuyu.listener.LoginAttributeListener"/>
<!-- 注册监听器并引入监听实现类 -->
<bean id="messageListener" class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
<property name="delegate" ref="dataSyncEventListener"/>
<property name="serializer" ref="stringRedisSerializer"/>
</bean>
<!-- 消息监听 -->
<redis:listener-container>
<!--指定消息处理方法,序列化方式及主题名称-->
<redis:listener ref="messageListener" method="onMessage" serializer="stringRedisSerializer" topic="__keyevent@0__:hset"/>
</redis:listener-container>
增加
MessageListener
的实现类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class SessionAttributeListener implements MessageListener {
// 注入session操作类
@Resource(name = "sessionRepository")
private RedisOperationsSessionRepository sessionRepository;
/**
* 监听redis的hset操作,并判断是否是向session中写入属性
*/
@Override
public void onMessage(Message message, byte[] pattern) {
if (message == null || message.getChannel() == null
|| message.getBody() == null) {
return;
}
// 获取频道发布的消息
String messageStr = new String(message.getBody());
// 向session中设置属性了
if(messageStr.startsWith("spring:session:sessions:")){
// 获取sessionId
String sessionId = messageStr.substring(messageStr.lastIndexOf(":") + 1);
// 获取session
Session addAttributeSession = sessionRepository.getSession(sessionId);
}
}
}