spring.session 随笔0 集成设计

news/2024/11/16 22:34:08/

0. 上个月划水时间关注的,最近断断续续的了解了一些

RUNOOB redis命令:APPEND
整合shiro实现分布式session同步(定制cacheManager)

我想想,还是照自己思绪发散的顺序开始描述这块的内容吧,可能侧重点有些奇怪。


由于工程使用的spring.boot.dependencies(BOM)版本是2.3.4 RELEASE,故这里使用的redisson库的版本为 3.14.1,望周知

1. redisson --> spring.data.redis

因为上个月搞分布式锁的时候,了解了下redis的java客户端实现redisson,感觉到各方面的支持还怪全面的。故这次也打算使用redisson作为redis连接框架,正好免了引入其他的redis客户端框架。

mvn repository中发现apache.redisson-spring-boot-starter并非spring官方所作,通过查看spring-data-redis的自动配置类RedisAutoConfiguration.class的源码,可以察觉spring-data-redis原生支持的redis客户端框架唯有jedis、lettuce。

package org.springframework.boot.autoconfigure.data.redis;@Configuration(proxyBeanMethods = false)
// RedisOperation 即 RedisTemplate 的实现接口
// 说明当我们注入一个 RedisTemplate 的时候,该配置类将生效
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {// @ConditionalOnMissingBean 允许我们自己配置 RedisTemplate// 这里的只是默认的// 不过,我们其实也可以不去自己配置这个类,redisson.spring本身也有实现类// 包括 RedisConnectionFactory,也是同理的@Bean@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);return template;}@Bean@ConditionalOnMissingBeanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}}

可能有 好事者(至少我自己) 会问: 为什么不是使用注解 @EnableRedissonHttpSession 来开启 redisson 的session支持呢?

  1. 其实吧,有,但是被注解摒弃了,我甚至还在网上看到有博客在介绍这个过时的配置类
package org.redisson.spring.session.config;/*** Deprecated. Use spring-session implementation based on Redisson Redis Data module** @author Nikita Koksharov**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
// 这个配置类也被摒弃了
@Import(RedissonHttpSessionConfiguration.class)
@Configuration
// 官方嫌弃
@Deprecated
public @interface EnableRedissonHttpSession {int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;String keyPrefix() default "";}

从javadoc可以得知,与spring的集成逻辑,应该走 redisson.data 模块

  1. 反过来想,Redisson 肯定会将推荐的集成方式(配置类)放在 redisson-spring-boot-srarter 的自动配置类 RedissonAutoConfiguration.class

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWZflL56-1687008021654)(img.png)]


spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.redisson.spring.starter.RedissonAutoConfiguration

其实如果你了解 spring.data 模块,也可以隐隐的捕获到这一信息(抽象层次: 应用领域 -> XxxTemplate -> XxxConnectFactory -> 底层的第三方客户端框架),不应该是第三方框架自己来组织 repository。这里说的 应用领域 可以狭义的理解为 spring.session

package org.redisson.spring.starter;@Configuration
@ConditionalOnClass({Redisson.class, RedisOperations.class})
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties({RedissonProperties.class, RedisProperties.class})
public class RedissonAutoConfiguration {private static final String REDIS_PROTOCOL_PREFIX = "redis://";private static final String REDISS_PROTOCOL_PREFIX = "rediss://";@Autowired(required = false)private List<RedissonAutoConfigurationCustomizer> redissonAutoConfigurationCustomizers;@Autowiredprivate RedissonProperties redissonProperties;@Autowiredprivate RedisProperties redisProperties;@Autowiredprivate ApplicationContext ctx;@Bean@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();template.setConnectionFactory(redisConnectionFactory);return template;}@Bean@ConditionalOnMissingBean(StringRedisTemplate.class)public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}@Bean@ConditionalOnMissingBean(RedisConnectionFactory.class)public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redisson) {return new RedissonConnectionFactory(redisson);}@Bean(destroyMethod = "shutdown")@ConditionalOnMissingBean(RedissonClient.class)public RedissonClient redisson() throws IOException {// ...}
}

简单来说,我们可以通过配置或者手动注入bean:RedissonClient来拉通…


加赠 spring.data.RedisTemplate 的族谱

在这里插入图片描述

2. spring.data.redis --> spring.session.data.redis

同上,redisson 自己搞的session相关的配置类也弃用了,彻底的走上了 spring.data 的整合套路

package org.redisson.spring.session.config;/*** Deprecated. Use spring-session implementation based on Redisson Redis Data module** @author Nikita Koksharov**/
@Configuration
@Deprecated
public class RedissonHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware {@Beanpublic RedissonSessionRepository sessionRepository(RedissonClient redissonClient, ApplicationEventPublisher eventPublisher) {// ...}@Overridepublic void setImportMetadata(AnnotationMetadata importMetadata) {// ...}
}

基于引入的 spring-session-data-redis 模块,那么实际将走的配置类也不再是 spring.session 的了 。简单看一下,整合spring.session和spring.data.redis后的全新配置类

package org.springframework.session.data.redis.config.annotation.web.http;/*** Exposes the {@link SessionRepositoryFilter} as a bean named* {@code springSessionRepositoryFilter}. In order to use this a single* {@link RedisConnectionFactory} must be exposed as a Bean.** @author Rob Winch* @author Eddú Meléndez* @author Vedran Pavic* @see EnableRedisHttpSession* @since 1.0*/
@Configuration(proxyBeanMethods = false)
// 可以看到继承了SpringHttpSessionConfiguration(spring.session配置类)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfigurationimplements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {// 这里隐掉了很多属性、方法:定时调度配置、事件相关、类加载相关的@Beanpublic RedisIndexedSessionRepository sessionRepository() {// 可以看到 redisTemplate 最终是在为 sessionRepository 服务的RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);// 设置应用的事件发布器(后续可以用来回调监听、完善session的生命周期)sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);if (this.indexResolver != null) {// indexResolver 是用于处理根据内存缓存的key找到对应redis中的key的sessionRepository.setIndexResolver(this.indexResolver);}if (this.defaultRedisSerializer != null) {// 序列化器sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);}sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);if (StringUtils.hasText(this.redisNamespace)) {// 一段字符串,表示 spring.session 的key(前缀)sessionRepository.setRedisKeyNamespace(this.redisNamespace);}// 这个配置就很贴心sessionRepository.setFlushMode(this.flushMode);sessionRepository.setSaveMode(this.saveMode);// 这个编号并不是 spring.datasource 的数据库,是redis库// 默认 0int database = resolveDatabase();sessionRepository.setDatabase(database);// 配置扩展走的是 开放配置 的方式this.sessionRepositoryCustomizers.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));return sessionRepository;}// redisConnectFactory 搁这里传递呢@Autowiredpublic void setRedisConnectionFactory(@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory.getIfAvailable();if (redisConnectionFactoryToUse == null) {redisConnectionFactoryToUse = redisConnectionFactory.getObject();}this.redisConnectionFactory = redisConnectionFactoryToUse;}
}

本来想到这,就直接转向 接入web.http呢,但是多看了一看SessionRepository,怎么说呢?还是再多看一眼源码吧,家人们…这javadoc写的真好

这个实现类,即我们上面配置类所注入的…

package org.springframework.session.data.redis;// 阅读理解,哈哈哈
// 这里大概描述了1个session会话所对应redis中使用的3个key
// 以及这些个key的作用说明
/*** <p>* A {@link org.springframework.session.SessionRepository} that is implemented using* Spring Data's {@link org.springframework.data.redis.core.RedisOperations}. In a web* environment, this is typically used in combination with {@link SessionRepositoryFilter}* . This implementation supports {@link SessionDeletedEvent} and* {@link SessionExpiredEvent} by implementing {@link MessageListener}.* </p>** <h2>Creating a new instance</h2>** A typical example of how to create a new instance can be seen below:** <pre>* RedisTemplate&lt;Object, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();** // ... configure redisTemplate ...** RedisIndexedSessionRepository redisSessionRepository =*         new RedisIndexedSessionRepository(redisTemplate);* </pre>** <p>* For additional information on how to create a RedisTemplate, refer to the* <a href = "https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/"* > Spring Data Redis Reference</a>.* </p>** <h2>Storage Details</h2>** The sections below outline how Redis is updated for each operation. An example of* creating a new session can be found below. The subsequent sections describe the* details.** <pre>* 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* </pre>** <h3>Saving a Session</h3>** <p>* Each session is stored in Redis as a* <a href="https://redis.io/topics/data-types#hashes">Hash</a>. Each session is set and* updated using the <a href="https://redis.io/commands/hmset">HMSET command</a>. An* example of how each session is stored can be seen below.* </p>** <pre>* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2* </pre>** <p>* In this example, the session following statements are true about the session:* </p>* <ul>* <li>The session id is 33fdd1b6-b496-4b33-9f7d-df96679d32fe</li>* <li>The session was created at 1404360000000 in milliseconds since midnight of 1/1/1970* GMT.</li>* <li>The session expires in 1800 seconds (30 minutes).</li>* <li>The session was last accessed at 1404360000000 in milliseconds since midnight of* 1/1/1970 GMT.</li>* <li>The session has two attributes. The first is "attrName" with the value of* "someAttrValue". The second session attribute is named "attrName2" with the value of* "someAttrValue2".</li>* </ul>*** <h3>Optimized Writes</h3>** <p>* The {@link RedisIndexedSessionRepository.RedisSession} keeps track of the properties* that have changed and only updates those. This means if an attribute is written once* and read many times we only need to write that attribute once. For example, assume the* session attribute "sessionAttr2" from earlier was updated. The following would be* executed upon saving:* </p>** <pre>* HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue* </pre>** <h3>SessionCreatedEvent</h3>** <p>* When a session is created an event is sent to Redis with the channel of* "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such that* "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the session id. The body of the event will be* the session that was created.* </p>** <p>* If registered as a {@link MessageListener}, then {@link RedisIndexedSessionRepository}* will then translate the Redis message into a {@link SessionCreatedEvent}.* </p>** <h3>Expiration</h3>** <p>* An expiration is associated to each session using the* <a href="https://redis.io/commands/expire">EXPIRE command</a> based upon the* {@link RedisIndexedSessionRepository.RedisSession#getMaxInactiveInterval()} . For* example:* </p>** <pre>* EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100* </pre>** <p>* You will note that the expiration that is set is 5 minutes after the session actually* expires. This is necessary so that the value of the session can be accessed when the* session expires. An expiration is set on the session itself five minutes after it* actually expires to ensure it is cleaned up, but only after we perform any necessary* processing.* </p>** <p>* <b>NOTE:</b> The {@link #findById(String)} method ensures that no expired sessions will* be returned. This means there is no need to check the expiration before using a session* </p>** <p>* Spring Session relies on the expired and delete* <a href="https://redis.io/topics/notifications">keyspace notifications</a> from Redis* to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent that ensures resources* associated with the Session are cleaned up. For example, when using Spring Session's* WebSocket support the Redis expired or delete event is what triggers any WebSocket* connections associated with the session to be closed.* </p>** <p>* Expiration is not tracked directly on the session key itself since this would mean the* session data would no longer be available. Instead a special session expires key is* used. In our example the expires key is:* </p>** <pre>* APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""* EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800* </pre>** <p>* When a session expires key is deleted or expires, the keyspace notification triggers a* lookup of the actual session and a {@link SessionDestroyedEvent} is fired.* </p>** <p>* One problem with relying on Redis expiration exclusively is that Redis makes no* guarantee of when the expired event will be fired if the key has not been accessed.* Specifically the background task that Redis uses to clean up expired keys is a low* priority task and may not trigger the key expiration. For additional details see* <a href="https://redis.io/topics/notifications">Timing of expired events</a> section in* the Redis documentation.* </p>** <p>* To circumvent the fact that expired events are not guaranteed to happen we can ensure* that each key is accessed when it is expected to expire. This means that if the TTL is* expired on the key, Redis will remove the key and fire the expired event when we try to* access the key.* </p>** <p>* For this reason, each session expiration is also tracked to the nearest minute. This* allows a background task to access the potentially expired sessions to ensure that* Redis expired events are fired in a more deterministic fashion. For example:* </p>** <pre>* SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe* EXPIRE spring:session:expirations1439245080000 2100* </pre>** <p>* The background task will then use these mappings to explicitly request each session* expires key. By accessing the key, rather than deleting it, we ensure that Redis* deletes the key for us only if the TTL is expired.* </p>* <p>* <b>NOTE</b>: We do not explicitly delete the keys since in some instances there may be* a race condition that incorrectly identifies a key as expired when it is not. Short of* using distributed locks (which would kill our performance) there is no way to ensure* the consistency of the expiration mapping. By simply accessing the key, we ensure that* the key is only removed if the TTL on that key is expired.* </p>** @author Rob Winch* @author Vedran Pavic* @since 2.2.0*/
public class RedisIndexedSessionRepositoryimplements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {// 隐掉所有代码 ...
}

3. spring.session.data.redis --> spring.web

有意思的是,这处的配置,居然给我在spring.session.data.redis配置类的父配置类中找到了(即spring.session的配置类)

package org.springframework.session.config.annotation.web.http;@Configuration(proxyBeanMethods = false)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {// 同样也隐去了一大坨别的东西,有空再慢慢看吧@PostConstructpublic void init() {CookieSerializer cookieSerializer = (this.cookieSerializer != null) ? this.cookieSerializer: createDefaultCookieSerializer();this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);}// spring.session.SessionRepositoryFilter 就是他了@Beanpublic <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {// 可以想到,我们注入了的 sessionRepository 最终也是为了给这个过滤器服务的SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);// 这又一个处理器,帮助request/response对象处理 sessionId 的逻辑实现sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);return sessionRepositoryFilter;}
}

4. spring.web --> javax.servlet

这块其实在 spring.webmvc 相关的随笔中多少沾了点边,这里走个流程吧。毕竟饿了还不能及时的吃饭的话,就有点上班的意思了,这不好。

在这里插入图片描述

吃饭之前,我们可以再想个问题:且不说我们准备从请求、响应的参数中提取sessionId并处理,如果我们想要在拦截器中扩展请求域的上下文参数。这时候,把数据、处理逻辑维护到拦截器里面肯定是不够OOP的,

于是乎,javax.servlet 也对此类提案,发布了可接入的扩展方式——给request/response之上直接一层wrapper,而我们的扩展就是针对这个wrapper,原有逻辑委托内部的request/response即可

// org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);// 接入 spring.session 的地方就发生在这里// 这俩都是当前类的静态内部类// 这还不完,该类还对原生的Session做了wrapper增强SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);try {filterChain.doFilter(wrappedRequest, wrappedResponse);}finally {wrappedRequest.commitSession();}
}

大体的内部结构如此,后面还有时间再看吧,先吃到嘴的饭了:)


http://www.ppmy.cn/news/440096.html

相关文章

pdf上的文字和图片内容怎么编辑

pdf文件虽然现在也是比较常见的一种文档&#xff0c;但还是有不少人都不知道怎么编辑pdf文件。对pdf文件的编辑其实和ppt一样很简单的&#xff0c;下面就来讲下pdf上的文字和图片内容都是怎么编辑的。 ​ 首先是编辑的工具&#xff0c;用来修改编辑pdf文件的工具也是有很多的&…

教你如何将图片制作成PDF文档

将一些图片文件转换成成为一个PDF格式文档&#xff0c;这也就是制作一个纯图片内容的文档&#xff0c;只是PDF文件接触的比较少&#xff0c;不知道怎样把图片转成PDF&#xff0c;其实要把图片转换成PDF还是很简单的。   一、用word或者ppt这里的编辑工具可以将图片合成一个P…

[PDF文件怎么编辑]如何在PDF文档中插入图片

收到一份PDF格式文档需要进行编辑&#xff0c;文件中只有文本内容&#xff0c;需要在页面中为对应的配图&#xff0c;也就是在PDF中添加图片&#xff0c;作为一个只懂得用阅读器来查看文档人来说&#xff0c;编辑PDF文件可谓是一件难事&#xff0c;这种格式的文件是如何编辑的呢…

PDF怎么编辑修改内容?教你一招轻松搞定

怎么编辑修改PDF文件中的内容呢&#xff1f;大家在日常中使用PDF文件的时候&#xff0c;如果发现文件中出现错误的内容时&#xff0c;想要编辑修改里面的文字&#xff0c;怎么才能做到呢&#xff1f;大家都知道PDF文件不能直接在里面编辑&#xff0c;所以有很多小伙伴想知道编辑…

如何编辑PDF文件?简单好用的编辑方法分享

如何编辑PDF文件内容呢&#xff1f;PDF文件一直是大家在办公中常用的一种文件格式&#xff0c;用来发送或者是浏览文件十分方便&#xff0c;但是大家在使用PDF文件是时候&#xff0c;难免会发现文件中出现一些错误的地方&#xff0c;又或者是察觉到可以用更好的内容代替&#x…

PDF如何编辑修改,怎么编辑PDF文字与图片

很多的小伙伴会私信小编询问小编关于PDF文件的修改技巧&#xff0c;在使用PDF文件的时候&#xff0c;往往是需要用到PDF编辑器的&#xff0c;编辑文件时&#xff0c;想要修改文件的内容包括文字内容以及图片内容&#xff0c;应该怎么去编辑呢&#xff0c;其实&#xff0c;还是很…

PDF文件中的图片如何修改

我们在日常编写或修改PDF文档时会发现&#xff0c;页面中的图片并不能像Word文档一样直接点击就进行删除或替换等操作&#xff0c;那么PDF文档中的图片应该如何修改呢&#xff1f; 跟Word文档不同&#xff0c;图片在PDF中的作用跟文本框一样都属于一个对象的编辑&#xff0c;因…

PDF文件如何插入图片?简单的操作方法

PDF文件如何插入图片呢&#xff1f;现在使用PDF文件的时候越来越多&#xff0c;但是对PDF文件如何插入图片还不是很了解&#xff0c;想要给PDF文件插入图片就可以使用迅捷PDF编辑器&#xff0c;下面小编就为大家分享一下PDF文件如何插入图片。 操作软件&#xff1a;迅捷PDF编辑…