自定義實現session持久化

自定義實現session持久化

使用場景

對於有登陸校驗的網站,tomcat 重啓以後,刷新頁面又得從新登陸,影響用戶體驗.java

緣由:
tomcat 的session 在內存中,tomcat重啓以後,內存中的session就銷燬了.致使登陸信息丟失git

session持久化的目的

對於存儲在session中的信息,服務器重啓以後,不會丟失. 好比用戶登陸以後,重啓tomcat服務器,刷新頁面,依然是登陸狀態.github

目標

  1. 從新實現session的經常使用操做,如 getAttribute(String s) , setAttribute(String s, Object o) , removeAttribute(String s) ;
  2. 編寫業務代碼時無侵入,也就是說,實際編寫業務代碼時不用使用專門的API,仍然像以前同樣操做HttpSession.

思路

  1. 增長過濾器 javax.servlet.Filter的實現類;
  2. 在請求以前包裝 HttpServletRequest,例如包裝類是SessionSyncRequestWrapper;
  3. 業務代碼中調用 getSession 時,就會調用包裝類SessionSyncRequestWrapper 的 getSession, 咱們只要在包裝類SessionSyncRequestWrapper 中,重寫getSession 便可.
  4. 自定義HttpSession 的包裝類 CustomSharedHttpSession,繼承HttpSession,
    在CustomSharedHttpSession 中重寫HttpSession的方法

實現方案

1. 自定義過濾器RequestbodyFilter,實現javax.servlet.Filter;

在RequestbodyFilter 中使用責任鏈設計模式編寫一套自定義的請求過濾器
主要類以下:
鏈條:redis

public class RequestFilterChain {
    private List<IRequestFilter> filterList = 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 中作了兩件事:tomcat

  1. 獲取session持久化方案,目前支持 database 和redis;
  2. 對請求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)方法時,
獲取的是咱們自定義的session處理類 CustomSharedHttpSession,
CustomSharedHttpSession 繼承 javax.servlet.http.HttpSessioncookie

4. CustomSharedHttpSession 重寫HttpSession的三個經常使用方法

  1. 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;
    }
  1. 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);
    }
  1. 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的構造方法有三個參數:session

  1. 原始的javax.servlet.http.HttpSession
  2. JSESSIONID :從請求頭中獲取的JSESSIONID;
  3. 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) 中的邏輯有點複雜,咱們進行詳細解析

在(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了,原本是有屬性值的,重啓一次,能夠獲取,重啓第二次,就沒法獲取了.

總結

  1. 咱們自定義的session持久化機制,是根據瀏覽器 cookie中 JSESSIONID 來關聯登陸信息的,
    可是tomcat每重啓次,session id會變化,因此才須要步驟(5)
  2. 咱們只須要實現ISharableSessionAPI,就能夠完成session持久化的功能

代碼:https://github.com/liuyu520/oa_framework

相關文章
相關標籤/搜索