Spring-Session 基础知识点 和 源码分析(下)
一、Spring-Session的使用场景:
场景一 、相同域名下相同项目实现session共享。
- 集群部署之后我们需要使用spring-session。
原因: 我们tomcat 服务器集群部署(处理动态资源)和nginx集群部署(处理静态资源)之后,配合我们的 主nginx负载均衡。我们的主nginx 根据 服务器的压力或者某种规则来进行分发请求。一个用户在一次会话当中,发出很多次请求,这些请求可能被分发到了 不同的 tomcat服务器进行处理,所以这时候需要 我们 spring-session 框架 将我们的session对象实现共享。
我们 9100 和 9200 部署 p2p项目模块,在9300中部署dataservice项目模块。
Nginx.conf 中配置的负载均衡:
我们的 127.0.0.1 是 windows 和 linux 通用的 地址,我们的 windows可以使用它,我们的linux也可以使用它,这个127.0.0.1 都代表本机的意思,代表windows本机或者linux本机。我们也可以在上面的配置中直接写linux的地址:192.168.242.128 。
知识点补充:nginx重启的命令。
- 向p2p项目中 添加 spring-Session框架所以来的jar包。不需要修改我们的pom文件,直接将jar导入到复制粘贴到 lib包中就可以了,因为我们的项目已经上线了,pom文件中所有的jar都下载完成了。
- 向9100和9200端口的tomcat中的p2p项目的web.xml中添加spring-session全局过滤器。
- 向配置文件包中,添加 appliction-session配置文件,还有redis.properties配置文件。
- 不需要使用监听器listener来加载我们的spring容器,因为我们的springMVC已经将spring容器加载了。 就是在 配置中央调度器的时候加载了我们的 spring总配置文件从而加载了 spring容器了。
上面的步骤就实现了:
配置多个tomcat(集群),然后使用nginx负载均衡。然后使用spring-session来实现我们的 session共享。因为是将我们的session对象存放进我们的redis中,这个redis是一个公共的部分,所以所有的tomcat都可以有需要,自己去redis中获取,但是我们存放的session最大空闲时间为 30 分钟。
场景二、同域名下不同项目实现session共享。 实质上和相同项目是一样的。
假如 域名为 www.myweb.com,项目都为 p2p,那么生成的session的sessionid存入cookie之后,这个cookie存放的位置是: myweb/com/www/ p2p/cookie对象
如果 两个不同的项目,一个是 pay一个是shop,我们第一次访问pay,生成的session将sessionId存放进cookie中,然后这个cookie被存放在 myweb/com/www/pay/下面。
然后同一个用户再发出一次请求访问,但是我们的这次的请求是请求 访问我们的shop项目,所以浏览器会从 myweb/com/www/shop/下面获取 cookie ,但是不会从 myweb/com/www/pay/下面获取cookie。
Domain + Path = cookie的存放路径。
意思是这样的:
- Localhost:9200/pay/setsession 这个请求的时候,我们的浏览器是从 myweb/com/www/pay/这个包结构下面获取cookie中的sessionId。
- Localhost:9200/shop/getsession 这个请求的时候,我们的浏览器是从Localhost:9200/shop/ 这个包结构下面获取cookie中的sessionId。
- 存放在不同包结构下面的cookie,cookie保存的sessionId也肯定不一样,从而导致从redis获取的session也肯定不一样。从而没有达到session共享的效果。
- 所以我们 需要让 每次发出请求后,返回的sessionid存放在 相同包结构下的cookie中,如:我们的cookie都放在根目录下,也就是myweb/com/www/下面,这样在一次会话中,发出所有的请求 浏览器都会去相同的包结构中获取cookie中的sessionId,这样就保证session的共享了。
这种情况造成的原因是:因为我们的项目不同,项目不同所以项目名不同,项目名不同从而导致了 tomcat的上下文根 不同,从而导致cookie的存放位置不同。存放位置不同所以cookie就不同,里面存放的sessionI就不同,从而session也不同,所以服务端就认为这是两个不同的请求。
********怎么设置我们cookie的存放位置呢?
我们的 cookie的存放位置 与Domain 和 Path有关。
在 spring-session 核心类 中 添加 property 属性。
这个核心类中有一个属性:
意思是 cookie的序列化方式,这个属性就是用来设置cookie的存放方式的,注意了:
核心类中的这个 cookie序列化属性 只是用来 设置 cookie的存放的方式的。
这个类是用来设置 cookie的默认序列化方式,也就是专门用来设置cookie默认存放路径的,注意了 是用来设置 cookie存放路径的。
核心类的属性和DefaultCookieSerializer这个类的属性配合起来就可以 设置我们的 cookie默认存放路径了 。
底层源码:
private String getDomainName(HttpServletRequest request) {
if (this.domainName != null) {
return this.domainName;
} else {
if (this.domainNamePattern != null) {
Matcher matcher = this.domainNamePattern.matcher(request.getServerName());
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
}
private String getCookiePath(HttpServletRequest request) {
return this.cookiePath == null ? request.getContextPath() + "/" : this.cookiePath;
}
场景三、同根域名,不同二级子域名下的项目实现session共享。
www.p2p.com 域名中 p2p.com 就是顶级域名,也叫做根域名,也可以叫做一级域名。
www不是二级域名。当www被替换了,那么替换的就是二级域名,如下:
Beijing.p2p.com 、 nanjing.p2p.com 、 tianjing.p2p.com
这三个域名中 beijing、tianjing、nanjing 是 二级域名 。
知识点补充;
在我们的 redis.conf中:
有一个 daemonize 属性,属性值为 默认为 no,意思是 不后台启动,我们如果想让redis默认后台启动,那么我们就 将我们的 redis.conf文件中的 daemonize属性值修改为 yes 。
然后就可以直接使用
来启动redis了,就不用 再 从最后 加一个 & 号 。
这是提供给我们测试使用的三个域名,我们在本地启动对应的tomcat,测试 根域名相同,但是二级域名不同,如何实现session的共享。
每个域名 对应 一个网站,每个网站 部署到两个不同的tomcat服务器上,然后使用nginx进行负载均衡。
测试前的准备:
我们需要修改 hosts文件,让我们的域名 指向到 我们的本地来,也就是 这三个域名对应的ip地址为127.0.0.1 。
根域名相同 ,二级域名不同,项目名不同:
项目名不同 导致的session不共享,我们的解决方案是 将 所有的cookie存放进 /目录下,也就是 根域名/二级域名/ cookie对象 这样子存放我们的cookie 。
当我们的 二级域名也不一样的时候,只是单单的修改cookie存放位置是不行的,还要修改cookie的存放路径,二级域名不同,根域名相同,那么为了保证每次请求保存的cookie相同,那么需要将cookie存放在公共相同的区域,所以 存放路径为: 根域名
存放路径 和 存放位置 相结合: 根域名/ 直接将我们的cookie存放在根域名下。
总之,要保证 我们的 session共享,就需要保证cookie是同一个,保证从一个cookie就需要 每次 存 和 每次 取 cookie 都要从 同一个地方 获取和存储,这样保证了cookie的位置。
如:
Beiijing.p2p.com:9100/pay/setsession 创建的session,而且在session中存放了xxx。
如何让我们的 nanjing.p2p.com:9200/pay/getSession 获取到 上面请求创建session,从而取到里面xxx。 这里就需要session共享,单单将session存放进redis是远远不够的,虽然我们的session 被存放进了 一个 共享的位置。 但是每次请求的推送过来的 sessionId被存放进不同位置上的cookie,如果让我们的 cookie也能放在一个共享的区域,每次存和取都能在同一个空间,这样就能保证同一个用户 对应 同一个cookie 对应 同一个session 。
***如何找到这个 公共的 共享区域呢?
分析我们的 两个域名:
Beijing . p2p . com 对应浏览器中的包: p2p/com/beijing/
Nanjing . p2p . com 对应浏览器中的包: p2p/com/nanjing/
包结构的公共区域为: P2p/com/
(1)P2p/com 就是 domainName。
(2)/ 就是 cookiePath 。
补充的知识点:
我们的 浏览器有 【 cookie存储 安全保护机制 】。
意思是: 我们不能自己给 上面这两个域名 自定义命名一个 自己命名的共享空间,如:我如果设定 所有的 cookie存放进 abc/com/ 这里。这样子我们的 cookie不会被我们的浏览器存储进去的,因为 在域名-1 下创建出来 cookie对象,最后存放的时候 也要 认祖归宗 落叶归根 到 域名-1 的 【根域名/】 来进行存储。 举个例子:你总不能 向京东 发送一个访问请求,然后将产生的cookie保存到 淘宝的域名根域名下面。
你访问京东产生的cookie只能存放在 京东的域名下,不能存放在淘宝的域名下。这就是cookie存放安全保护机制 。
思路:
我们二级域名不一样,那我们就存放在 相同的顶级域名下呗,哪里相同我们就存在哪里呗。
只要 domain+path 保持一致,两次请求cookie的存取位置都是相同的,那么就session共享了。
*************项目名不相同,二级域名不相同,顶级域名相同,如何实现session共享,步骤如下:
场景四、不同根域名下的项目实现session共享。
如:
这种情况 我们的spring-session框架就不支持了,因为 这种情况:存放 cookie的包 从 根部就已经不相同了,那么根底下的子包就更不相同了,所以domain找不到 相同和共享的包空间了, 所以 我们在 spring-session的核心类中 定义 cookie 存放路径属性的时候,就没办法指定 属性 domainName属性的值,这个值不能指定 我们的session就没有办法共享,所以这种情况 如果 域名从根域名就开始不同了,那么我们的 spring-session就不支持了。
我们 通过 www.p2p.com访问我们的服务器 。那么我们的生成的cookie只能往 p2p.com根域名下面去写。
我们通过 www.web.com访问我们的服务器,那么生成的cookie只能往web.com根域名中去写。
以此类推.......
(1)这种情况 我们 可以 使用 【单点登录SSO】来解决我们的session共享问题。
(2)【单点登录SSO】是什么意思呢?
多个系统应用中(如:天猫,淘宝,支付宝),用户只需要登录一次,其他与这个应用相互信任的应用都会自动登录了。
就好像 我们 登录了 支付宝,那么我们的再登录我们的 淘宝和天猫就可以点击直接登录了。
【单点登录】的实现,我们必须开发一套单点登录系统。一般很大的公司才会有这样的单点登录系统,像阿里巴巴这样才有多个网站和应用。
Spring - Session框架 的 处理流程:
Spring-session框架需要运行必须拥有而且必不可少的东西:
- 全局过滤器:
- spring容器::
使用 Listener监听器 来 读取我们的 spring 总配置文件 ,然后创建对应的 spring容器。
-
我们 用户发出的请求 首先会被我们的 过滤器接收到 。
过滤器DelegatingFilterProxy 继承了 GenericFilterBean 这个类,然后GenericFilterBean这个类又实现了Filter接口,所以我们的DelegatingFilterProxy 这个类也是一个Filter类,也就是一个过滤器 。
只要是过滤器就有一下三种方法: init()初始化方法 , doFilter()过滤方法 和 destory()关闭时销毁用的方法 。(1)GenericFilterBean 类的 init初始化方法中:核心代码:
但是这个方法在我们的GenericFilterBean 类中,方法体中没有添加任何的代码。所以推测出我们的 子类spring-session全局过滤器DelegatingFilterProxy 应该是重写了这个方法initFilterBean()。
我们子类中 重写这个方法的代码如下:
当我们的全局过滤器在初始化的时候就会调用这个这个方法。
全局过滤器刚被加载的时候,会调用init方法,所以也会调用initFilterBean这个方法,这个方法中涉及这段代码:
刚开始 我们的 delegate对象和targetBeanName对象 肯定都为空。
这时候 是调用 getFilterName来获取当前这个过滤器的名称,然后将名称赋值为targetBeanName对象。
TargetBeanName = filter名称 ***到这一步我们的 获取到了 Filter的名称了 。
-
这段代码是来找 web容器,也就是spring容器,通过FindWebApplicationContext方法。 ****到这一步我们 获取到了spring容器了.
方法initDelegate的源码:
使用 spring容器wac,来获取到一个id为TargetBeanName ,类型为Filter过滤器的 bean对象。
***到这一步 我们的获取到了 全局过滤器的 bean对象 。
-
初始化都做了什么?
也就是 我们的 全局过滤器初始化的时候:先获取了 本过滤器的名称,然后有获取到了spring容器,然后根据名称和类型获取到了 过滤器对应的 bean对象 。
上面的步骤初始化已经完成了
下面是 发送请求的时候,过滤器会调用 doFilter方法:
全局过滤器的父类是一个抽象类,内部没有重写doFilter方法。 那么这个doFilter方法肯定是有 我们的全局过滤器 重写的 。
-
全局过滤器中 重写的 doFilter方法,如下:
这个dofilter方法中有一个 代理类方法:
-
这个代理类方法的内部是:
这个代理类方法的内部是:
意思是:我们调用DelegatingProxy过滤器中的dofilter方法实质上是调用delegate中的dofilter方法。
其实这个 invokeDelegate方法是一个代理。
Delegate 就是 我们上面 从init初始化方法中获取到的 目标过滤器,也就是web.xml中配置的过滤器。 也就是最终调用的是 spring容器中bean对象对应的filter中的dofilter方法。
这个代理类 代理的是名字叫做:
filter过滤器。 这个过滤器是在我们的spring容器中的一个bean对象。所以真正工作的就是我们的是
这个过滤器。
-
所以我们开始分析:springsessionrespositoryFilter过滤器:
这个类是我们 Spring-session框架 最核心 的 类 。
-
我们在 核心类的父类中找到了这个 真正工作的 过滤器:
-
@Bean 注解 :
Bean的id是方法名,class是方法的返回值类型。
-
由上面的源码我们可以看出:
-
下面我们开始分析
中的代码内容: 这个过滤器中不存在 init方法,所以我们的 这个过滤器 不存在 初始化的问题,它继承了父类中的dofilter方法,dofiler中的内容:
-
父类dofilter方法中的核心逻辑是:
但是这个方法是一个抽象方法:
,所以这个方法 应该在我们的子类
中被重写。所以说子类调用父类的dofilter方法实质上是调用了doFilterInternal这个方法。
-
子类重写的内容如下:
-
继承关系:
-
这个是构造自己的 request 和response,将原来的 request 和response覆盖掉。将 request 和response中的方法也给替换掉了,替换成Spring-session框架中的了。
-
这个方法就是用来将我们的session存放在redis中。
这个方法中真正保存session的方法是save()方法:
-
Save方法中使用的 saveDelta这个方法进行session保存的:
-
这个方法中最核心的代码是:
-
这里保存的是一个 hash结构的session。因为是使用 putAll进行保存的:
PutAll这个方法是在 spring-data-redis jar包中。
然后这个方法中又调用了 jedis 的jar中的方法 hMset()方法,以为操作redis都是用的我么你的jedis进行间接操作的。
-
-
所以保存的session是hash结构:如下图
-
Spring-Session的执行流程:
上面就是 Spring-Session的 全部操作过程。
-
最后需要注意的是: