背景:
之前一直采用通过注解的方式配置Spring环境下的子域名共享,其基本思路是通过将session放入redis中,然后将使用HTTPSESSION更改为使用SpringSession的方式,使得不同节点共享Session。
一,使用XML的配置方式:
首先需要配置Redis的连接工厂(在这里采用的redis的哨兵模式):
<!-- ############################ Redis 连接池 ↓ ############################### -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.pool.maxTotal}" />
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<property name="numTestsPerEvictionRun" value="${redis.pool.numTestsPerEvictionRun}" />
<property name="timeBetweenEvictionRunsMillis" value="${redis.pool.timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${redis.pool.minEvictableIdleTimeMillis}" />
<property name="softMinEvictableIdleTimeMillis" value="${redis.pool.softMinEvictableIdleTimeMillis}" />
<property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>
<!-- ############################ Redis 连接池 ↑ ############################### -->
<!-- ############################ Redis 哨兵集群 ↓ ############################### -->
<bean id="sentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<property name="master">
<bean class="org.springframework.data.redis.connection.RedisNode">
<!-- 这个值要和Sentinel中指定的master的名称一致。 -->
<property name="name" value="${redis.master.name}"></property>
</bean>
</property>
<!-- 这里是指定Sentinel的IP和端口,不是Master和Slave。 -->
<property name="sentinels">
<set>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="${redis.sentinel.host1}"></constructor-arg>
<constructor-arg name="port" value="${redis.sentinel.port1}"></constructor-arg>
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="${redis.sentinel.host2}"></constructor-arg>
<constructor-arg name="port" value="${redis.sentinel.port2}"></constructor-arg>
</bean>
</set>
</property>
</bean>
<!-- ############################ Redis 哨兵集群 ↑ ############################### -->
<!-- ############################ Redis ConnectionFactory ↓ ############################### -->
<bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg name="sentinelConfig" ref="sentinelConfiguration"></constructor-arg>
<constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
</bean>
<!-- ############################ Redis ConnectionFactory ↑ ############################### -->
如果只是想实现spring-session各节点共享,而不需要实现子域名的跨域访问,则只需注册RedisHttpSessionConfiguration这个Bean即可使用:
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="6000"/>
<property name="httpSessionListeners">
<list>
<bean class="com.bsmartd.handler.SessionAccessListener"/>
</list>
</property>
</bean>
而后,需要在web.xml中配置过滤器,将所有的请求url都应用这种spring-session,至于这方面的原理稍后在介绍完java代码方式的配置后一起进行介绍:
<!-- spring redis session共享 -->
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
至此,通过xml的方式配置Spring-Session共享的方式已经实现,使用时按照正常的request请求放入
request.getSession().setAttribute("springsession", object);
如果我们需要进行主子域名的共享的话,需要配置Cookie策略,并将其应用到RedisHttpSessionConfiguration中,所以我们需要改写下上面注入RedisHttpSessionConfiguration的配置:
1)首先需要注册Cookie解析器,配置主域名的domain,实现主子域名共享一个cookiepath:
<!-- spring redis session共享 -->
<bean id="defaultCookieSerializer"
class="org.springframework.session.web.http.DefaultCookieSerializer">
<property name="cookieName" value="MySESSION" />
<property name="domainName" value="mydomain.com" />
<property name="cookiePath" value="/" />
</bean>
2)需要注册CookieSession策略:
<!-- spring redis session共享 -->
<bean id="cookieHttpSessionStrategy"
class="org.springframework.session.web.http.CookieHttpSessionStrategy">
<property name="cookieSerializer" ref="defaultCookieSerializer" />
</bean>
3)将session解析策略及在redis中的存放位置都配置到RedisHttpSessionConfiguration之中:
<!-- 将session放入redis -->
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="6000"/>
<property name="redisNamespace" value="MySessionNameSpace"></property>
<property name="httpSessionStrategy" ref="cookieHttpSessionStrategy"/>
<property name="defaultRedisSerializer">
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
</property>
</bean>
然后可以实现主子域名分布式环境下的session共享问题,例如:http://web1.mydomain.com/ 与 http://web2.mydomain.com/ ,即可以实现session共享。
二,使用java代码的配置方式:
我们一步步的进行配置多节点的session共享,首先需要配置Spring-Redis-Session的配置类:
@Configuration
public class RedisConfig{
}
然后需要注册Redis的连接工厂,连接池及哨兵集群Bean:
/**
* Redis连接工厂
*/
@Bean
public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig,RedisSentinelConfiguration redisSentinelConfiguration){
return new JedisConnectionFactory(redisSentinelConfiguration,jedisPoolConfig);
}
/**
* Redis 连接池
* @param property
* @return
*/
@Bean
public JedisPoolConfig jedisPoolConfig(RedisProperty property){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(property.getMaxTotal());
jedisPoolConfig.setMaxIdle(property.getMaxIdle());
jedisPoolConfig.setNumTestsPerEvictionRun(property.getNumTestsPerEvictionRun());
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(property.getTimeBetweenEvictionRunsMillis());
jedisPoolConfig.setMinEvictableIdleTimeMillis(property.getMinEvictableIdleTimeMillis());
jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(property.getSoftMinEvictableIdleTimeMillis());
jedisPoolConfig.setMaxWaitMillis(property.getMaxWaitMillis());
jedisPoolConfig.setTestOnBorrow(property.isTestOnBorrow());
return jedisPoolConfig;
}
/**
* Redis哨兵集群
*/
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration(RedisProperty property){
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
// 主节点
redisSentinelConfiguration.setMaster(()-> property.getMasterName());
// Sentinel哨兵集群
Set<RedisNode> redisNodes = new HashSet<>();
redisNodes.add(new RedisNode(property.getHost1(),property.getPort1()));
redisNodes.add(new RedisNode(property.getHost2(),property.getPort2()));
redisNodes.add(new RedisNode(property.getHost3(),property.getPort3()));
redisNodes.add(new RedisNode(property.getHost4(),property.getPort4()));
redisSentinelConfiguration.setSentinels(redisNodes);
return redisSentinelConfiguration;
}
然后需要在类注解上添加@EnableRedisHttpSession注解:
@Configuration
@EnableRedisHttpSession
public class RedisConfig{
}
然后需要配置代理过滤器,将所有的请求都映射到spring-session的filter之上:
public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
}
该类不需要进行任何实现,只需要继承一下即可。
实现主子域名session共享:
需要在RedisConfig中注入相关的Bean(cookie解析器,httpsession策略等)
/**
* 配置Spring-Session共享
*/
@Bean
public CookieSerializer defaultCookieSerializer(){
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setCookieName("MyESSION");
defaultCookieSerializer.setDomainName("mydomain.com");
defaultCookieSerializer.setCookiePath("/");
return defaultCookieSerializer;
}
@Bean
public CookieHttpSessionStrategy cookieHttpSessionStrategy(CookieSerializer defaultCookieSerializer){
CookieHttpSessionStrategy cookieHttpSessionStrategy = new CookieHttpSessionStrategy();
cookieHttpSessionStrategy.setCookieSerializer(defaultCookieSerializer);
return cookieHttpSessionStrategy;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
若是我们需要将session存放于Redis的特定目录之下,则需在使用EnableRedisHttpSession标签的时候指定redis的名称空间:
@EnableRedisHttpSession(redisNamespace = "MySessionNameSpace")
测试效果:
启动两个tomcat,两个项目,通过nginx代理到同domain的两个不同的二级域名之下,通过获取sessionid发现两个session的sessionid是一个,说明两个不同二级域名之下两个不同的应用节点实现了session共享。
查看redis之下确实存在对应结点的session信息:
原理分析:
从代理过滤器DelegatingFilterProxy谈起,在xml配置方式中我们配置了:
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
或者代码方式的extends AbstractHttpSessionApplicationInitializer,都是使用
DelegatingFilterProxy代理过滤器,它会将真正的请求代理到springSessionRepositoryFilter这个过滤器之中,所以对于spring-session的处理都是springSessionRepositoryFilter将请求进行了相应的处理。那么这个springSessionRepositoryFilter在代码方式中是如何进行注入的?这就要看我们使用spring-session的标签EnableRedisHttpSession的源码了:
原来在我们使用@EnableRedisHttpSession注解时就自动将springSessionRepositoryFilter暴露出来,这样:
public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
}
通过这个类,在项目启动的时候该类会自动启动,去寻找到对应的springSessionRepositoryFilter,从而使用请求的过滤绑定。
@EnableRedisHttpSession标签上自定义的有redisNameSpace的字段属性,即对应着redis存储的命名空间。通过@Configuration我们可以看出这是一个配置类,所以它可以注册Bean,也可以引用其它的配置类,在这里很关键的一点是我们看到它引用了@Import(RedisHttpSessionConfiguration.class)这个配置类,并且@EnableRedisHttpSession即将spring-redis-session的所有配置信息进行设置,我们可能需要自定义Session配置的相关属性,比如我们在前面设置的主子域名的domain,redis的命名空间,session的默认解析器等等,这就需要我们自己扩展RedisHttpSessionConfiguration的相关配置类。
继续看RedisHttpSessionConfiguration的源码,
我们看到RedisHttpSessionConfiguration继承了ImportAware的接口,按照spring生命周期的加载规则,Aware接口的首先加载,
可以看到首先便执行了RedisHttpSessionConfiguration的setImportMetadata的方法:
该方法即将@EnableRedisHttpSession(redisNamespace = “MySessionNameSpace”)标签上设置的redisnamespace,过期使劲等信息赋值到RedisHttpSessionConfiguration对象之中;
通过这段代码我们得知配置了redis的模板解析,如果我们传入解析器则使用。
我们看到RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration类,而该类实现了ApplicationContextAware接口,即会将spring的上下文注入到此配置类中。
这是一个配置类,所以它可以注册Bean,在源码中我们看到:
这是很关键的一个注册Bean,即我们前面说的@EnableRedisHttpSession标签会暴露出springSessionRepositoryFilter的过滤器,即是该处的实现。我们看到它设置了相关的session解析策略,其实this指向的,在该配置类的下部我们看到了通过@autowired的方式注入了相关的bean:
这就对应了我们之前在RedisConfig配置的bean:
@Bean
public CookieSerializer defaultCookieSerializer(){
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setCookieName("MyESSION");
defaultCookieSerializer.setDomainName("mydomain.com");
defaultCookieSerializer.setCookiePath("/");
return defaultCookieSerializer;
}
@Bean
public CookieHttpSessionStrategy cookieHttpSessionStrategy(CookieSerializer defaultCookieSerializer){
CookieHttpSessionStrategy cookieHttpSessionStrategy = new CookieHttpSessionStrategy();
cookieHttpSessionStrategy.setCookieSerializer(defaultCookieSerializer);
return cookieHttpSessionStrategy;
}
你又会好奇,我们不止配置了session策略,我们还配置了session的默认解析器,而在SpringHttpSessionConfiguration中不存在这个注入,
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
以为这属于redisconfiguration的配置项,所以该项bean的注入发生在RedisHttpSessionConfiguration配置类的引用中:
通过@Qualifier的方式注入,我们得知上面的bean必须命名为springSessionDefaultRedisSerializer(spring的默认bean命名规则)。
当所有的配置项都配置完成,并且注入进来后,即开启了SpringHttpSessionConfiguration的init操作,并结合RedisHttpSessionConfiguration通过@Bean的方式将RedisMessageListenerContainer、RedisTemplate、RedisOperationsSessionRepository 等注入到Spring容器中。
我们再回头看一下springSessionRepositoryFilter这个过滤器,只所以再次分析它,是因为它非常关键,可以说是spring-session实现特别关键的一步:
springSessionRepositoryFilter的作用:
通过返回值我们看到其返回类型为SessionRepositoryFilter,SessionRepositoryFilter的作用就是替换容器默认的javax.servlet.http.HttpSession支持为org.springframework.session.Session。SessionRepositoryFilter含有很多内部类:SessionRepositoryResponseWrapper、SessionRepositoryRequestWrapper、HttpSessionWrapper。
SessionRepositoryResponseWrapper与SessionRepositoryRequestWrapper通过对request与response请求接口的重新编写(HttpServletRequest中的getSession),
重写了HttpServletRequestWrapper中的getRequestedSessionId,getSession等方法,实现了之前使用HttpSession改用成使用Spring Session的方式:
所以,springSessionRepositoryFilter的作用实质是将原有的request请求和response都被重新进行了包装,将HTTPSession替换为Spring Session。
终于写完了,自己也是刚开始看源码,所以很多地方都是很粗浅,本文后半部分写原理的逻辑有点混乱,需要进一步的厘清相关的思路。