自定义实现session持久化
使用场景
对于有登录校验的网站,tomcat 重启之后,刷新页面又得重新登录,影响用户体验.
原因:
tomcat 的session 在内存中,tomcat重启之后,内存中的session就销毁了.导致登录信息丢失session持久化的目的
对于存储在session中的信息,服务器重启之后,不会丢失. 比如用户登录之后,重启tomcat服务器,刷新页面,依然是登录状态.
目标
- 重新实现session的常用操作,如
getAttribute(String s)
,setAttribute(String s, Object o)
,removeAttribute(String s)
; - 编写业务代码时无侵入,也就是说,实际编写业务代码时不用使用专门的API,仍然像之前一样操作HttpSession.
思路
- 增加过滤器 javax.servlet.Filter的实现类;
- 在请求之前包装 HttpServletRequest,例如包装类是SessionSyncRequestWrapper;
- 业务代码中调用 getSession 时,就会调用包装类SessionSyncRequestWrapper 的 getSession, 我们只要在包装类SessionSyncRequestWrapper 中,重写getSession 即可.
- 自定义HttpSession 的包装类 CustomSharedHttpSession,继承HttpSession, 在CustomSharedHttpSession 中重写HttpSession的方法
实现方案
1. 自定义过滤器RequestbodyFilter,实现javax.servlet.Filter;
在RequestbodyFilter 中使用责任链设计模式编写一套自定义的请求过滤器
主要类如下: 链条:public class RequestFilterChain { private ListfilterList = new ArrayList<>(); private int index = 0; private boolean hasAddDefaultFilter = false; public RequestFilterChain addFilter(IRequestFilter filter) { if (hasAddDefaultFilter) { throw new RuntimeException("自定义过滤器必须在默认过滤器之前添加"); } this.filterList.add(filter); return this; } public RequestFilterChain addDefaultFilter(IRequestFilter filter) { this.filterList.add(filter); hasAddDefaultFilter = true;// DefaultFormRequestWrapperFilter defaultDaoFilter = (DefaultFormRequestWrapperFilter) filter; return this; } public void reset() { this.index = 0; } private IRequestFilter next() { if (index == filterList.size()) { return null; } IRequestFilter filter = filterList.get(index); index++; return filter; } public void doFilter(HttpPutFormContentRequestWrapper request, HttpServletResponse response) throws IOException, ServletException { IRequestFilter filter = next(); if (null == filter) { System.out.println("结束 index :" + index); return; } filter.doFilter(request, response, this); }}
请求处理接口 :
public interface IRequestFilter { void doFilter(HttpPutFormContentRequestWrapper request, HttpServletResponse response, RequestFilterChain filterChain) throws IOException, ServletException;}
在RequestbodyFilter 中处理链条:
wrapper.setChain(chain); RequestFilterChain requestFilterChain = new RequestFilterChain(); addRequestFilter(requestFilterChain); HttpServletRequest httpServletRequest1 = (HttpServletRequest) request; boolean isLocalIp = WebServletUtil.isLocalIp(httpServletRequest1); if (!isLocalIp) { requestFilterChain.addFilter(new DecideUseCacheWhenOvertimeFilter()); } requestFilterChain.addDefaultFilter(new DefaultFormRequestWrapperFilter()); requestFilterChain.doFilter(wrapper, (HttpServletResponse) response); wrapper.resetCustom();
2. HttpSessionSyncShareFilter实现自定义请求接口 IRequestFilter
HttpSessionSyncShareFilter 中做了两件事:
- 获取session持久化方案,目前支持 database 和redis;
- 对请求request进行包装,包装成为 SessionSyncRequestWrapper;
HttpSessionSyncShareFilter中核心方法:
@Override public void doFilter(HttpPutFormContentRequestWrapper request, HttpServletResponse response, RequestFilterChain filterChain) throws IOException, ServletException { ISharableSessionAPI sharedSessionAPI = getSharableSessionAPI(request, this.sessionImplType); System.out.println("sharableSessionAPI :" + sharedSessionAPI.getClass().getSimpleName()); SessionSyncRequestWrapper sessionSyncRequestWrapper = new SessionSyncRequestWrapper(request, sharedSessionAPI); if (null == sessionSyncRequestWrapper.getChain()) {//NOTICE:一定要有这个判断 sessionSyncRequestWrapper.setChain(request.getChain()); } filterChain.doFilter(sessionSyncRequestWrapper, response); }
3. 请求包装类SessionSyncRequestWrapper
在SessionSyncRequestWrapper 中重写 getSession(boolean create)
,这样在业务代码中调用getSession(boolean create)方法时,
4. CustomSharedHttpSession 重写HttpSession的三个常用方法
- getAttribute
/*** * 需要重写 * @param s * @return */ @Override public Object getAttribute(String s) { Object o1 = null; if (null == this.getHttpSession()) { o1 = sharedSessionAPI.getSessionAttribute(this.JSESSIONID, s); return o1; } Object o = this.httpSession.getAttribute(s); if (o == null) { String currentSessionId = this.httpSession.getId(); o = sharedSessionAPI.getSessionAttribute(currentSessionId, s); if (null != o) { //使用新的 JSESSIONID 保存到redis 中 this.setAttribute(s, o); return o; } if ((!currentSessionId.equals(this.JSESSIONID))) { Object o2 = sharedSessionAPI.getSessionAttribute(this.JSESSIONID, s); //RedisCacheUtil.getSessionAttribute(this.JSESSIONID + s); if (null != o2) { this.httpSession.setAttribute(s, o2); //此时 this.JSESSIONID有值,但是currentSessionId没有值,所有要手动同步 sharedSessionAPI.setSessionAttribute(currentSessionId, s, o2); o = o2; } } } return o; }
- setAttribute
/** * 需要重写 * * @param s * @param o */ @Override public void setAttribute(String s, Object o) { String sessionId = null; if (null == this.httpSession) { sessionId = this.JSESSIONID; } else { this.httpSession.setAttribute(s, o); sessionId = this.httpSession.getId(); } sharedSessionAPI.setSessionAttribute(sessionId, s, o); }
- removeAttribute
@Override public void removeAttribute(String s) { if (null != this.httpSession) { this.httpSession.removeAttribute(s); String sessionId = this.httpSession.getId(); sharedSessionAPI.setSessionAttribute(sessionId, s, null); } sharedSessionAPI.setSessionAttribute(this.JSESSIONID, s, null); }
难点解析
CustomSharedHttpSession的构造方法有三个参数:
- 原始的javax.servlet.http.HttpSession
- JSESSIONID :从请求头中获取的JSESSIONID;
- ISharableSessionAPI:接口,自定义session属性和值的操作.
ISharableSessionAPI 接口 :
/*** * see CustomSharedSessionAPI,CustomSharedHttpSession */public interface ISharableSessionAPI { Object getSessionAttribute(String sessionId, String key); void setSessionAttribute(String sessionId, String s, Object o);}
getAttribute(String s) 中的逻辑有点复杂,我们进行详细解析
![](https://oscimg.oschina.net/oscnet/d399f5c6ed9d0268ed75b3467713dcf8583.jpg)
在(1)中,尝试获取原始的session,
如果原始的session为空,则调用ISharableSessionAPI获取属性值,直接返回, 并没有判断属性值是否为空.在(2)中,如果原始session不为空,则从原始session获取属性值o,
如果o不为空则直接返回, 因为已经取到值了,没有必要从ISharableSessionAPI 中获取.在(3)中,如果o为空,就需要尝试从ISharableSessionAPI 中获取了;
先拿原始session的id 作为key,来获取, 如果属性值不为空,则同步到原始session中,因为刚才在(2)中得知原始session没有属性值. 然后返回.进入步骤(4)中,说明 以原始session的id 作为key没有获取到值,
那么以this.JSESSIONID 作为key,调用ISharableSessionAPI 获取属性值, 如果获取到值,则同步到原始session,(5)中为什么又要设置一遍ISharableSessionAPI的保存呢?
这里是关键!!! 这里是关键!!! 这里是关键!!!原因如下:
我们按照时间顺序走一遍流程:浏览器第一次访问服务器, 服务器生成原始session,比如key为AAA,
登录时,保存username到原始session 和ISharableSessionAPI 中.此时tomcat重启了,
第二次访问,浏览器带过去cookie ,sessionid :AAA, 但是tomcat重启之后,原来的session属性都没了,所以通过AAA获取不到属性值, tomcat会生成新的session id : BBB 于是以AAA为key,调用ISharableSessionAPI,成功获取到值,并且同步到session id : BBB,如果没有步骤(5)的话, (5)的作用是把属性值同步到key为AAA的ISharableSessionAPI中.
此时tomcat又重启了,
第二次访问,浏览器带过去cookie ,sessionid :BBB, tomcat重启之后,原来的session属性都没了,所以通过BBB获取不到属性值, tomcat会生成新的session id : CCC 于是以BBB为key,调用ISharableSessionAPI,不可能获取到值,这就出现bug了,本来是有属性值的,重启一次,可以获取,重启第二次,就无法获取了.
总结
- 我们自定义的session持久化机制,是根据浏览器 cookie中 JSESSIONID 来关联登录信息的, 但是tomcat每重启次,session id会变化,所以才需要步骤(5)
- 我们只需要实现ISharableSessionAPI,就可以完成session持久化的功能
代码: