1. 程式人生 > >spring-session原始碼解讀-5

spring-session原始碼解讀-5

session通用策略

Session在瀏覽器通常是通過cookie儲存的,cookie裡儲存了jessionid,代表使用者的session id。一個訪問路徑只有一個session cookie(事實上在客戶端就只有一個cookie,jsessionid是作為cookie值的一部分,這裡把cookie抽象成類似伺服器端的實現),也就是一個訪問路徑在一個瀏覽器上只有一個session,這是絕大多數容器對session的實現。而spring卻可以支援單瀏覽器多使用者session。下面就看看spring是怎樣去支援多使用者session的。

對多使用者session的支援

spring session通過增加session alias概念來實現多使用者session,每一個使用者都對映成一個session alias。當有多個session時,spring會生成“alias1 sessionid1 alias2 sessid2…….”這樣的cookie值結構。

spring session提交時如果有新session生成,會觸發onNewSession動作生成新的session cookie

public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
        Set<String> sessionIdsWritten = getSessionIdsWritten(request);
        if(sessionIdsWritten.contains(session.getId())) {
            return;
        }
        sessionIdsWritten.add(session.getId());

        Map<String
,String> sessionIds = getSessionIds(request); String sessionAlias = getCurrentSessionAlias(request); sessionIds.put(sessionAlias, session.getId()); Cookie sessionCookie = createSessionCookie(request, sessionIds); response.addCookie(sessionCookie); }

a) 確保已經存在cookie裡的session不會再被處理。
b) 生成一個包含所有alias的session id的map,並通過這個map構造新的session cookie值。

createSessionCookie會根據一個alias-sessionid的map去構造session cookie。

private Cookie createSessionCookie(HttpServletRequest request,
            Map<String, String> sessionIds) {
        //cookieName是"SESSION",spring的session cookie都是
        //以"SESSION"命名的
        Cookie sessionCookie = new Cookie(cookieName,"");
        //省略部分非關鍵邏輯

        if(sessionIds.isEmpty()) {
            sessionCookie.setMaxAge(0);
            return sessionCookie;
        }

        if(sessionIds.size() == 1) {
            String cookieValue = sessionIds.values().iterator().next();
            sessionCookie.setValue(cookieValue);
            return sessionCookie;
        }
        StringBuffer buffer = new StringBuffer();
        for(Map.Entry<String,String> entry : sessionIds.entrySet()) {
            String alias = entry.getKey();
            String id = entry.getValue();

            buffer.append(alias);
            buffer.append(" ");
            buffer.append(id);
            buffer.append(" ");
        }
        buffer.deleteCharAt(buffer.length()-1);

        sessionCookie.setValue(buffer.toString());
        return sessionCookie;
    }

a) 當session被invalidate,可能會存在seesionids為空的情況,這種情況下將session cookie的最大失效時間設成立即。
b) 如果只有一個session id,則和普通session cookie一樣處理,cookie值就是session id。
c) 如果存在多個session id,則生成前文提到的session cookie值結構。

session cookie的獲取

getSessionIds方法會取出request裡的session cookie值,並且對每種可能的值結構進行相應的格式化生成一個key-value的map。

public Map<String,String> getSessionIds(HttpServletRequest request) {
        Cookie session = getCookie(request, cookieName);
        String sessionCookieValue = session == null ? "" : session.getValue();
        Map<String,String> result = new LinkedHashMap<String,String>();
        StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " ");
        //單使用者cookie的情況
        if(tokens.countTokens() == 1) {
            result.put(DEFAULT_ALIAS, tokens.nextToken());
            return result;
        }
        while(tokens.hasMoreTokens()) {
            String alias = tokens.nextToken();
            if(!tokens.hasMoreTokens()) {
                break;
            }
            String id = tokens.nextToken();
            result.put(alias, id);
        }
        return result;
    }
  1. 對單使用者session cookie的處理,只取出值,預設為是預設別名(預設為0)使用者的session。
  2. 對多使用者,則依據值結構的格式生成alias-sessionid的map。
  3. 以上兩種格式化都是對建立session的逆操作。

getCurrentSessionAlias用來獲取當前操作使用者。可以通過在request裡附加alias資訊,從而讓spring可以判斷是哪個使用者在操作。別名是通過”alias name=alias”這樣的格式傳入的,alias name預設是_s,可以通過setSessionAliasParamName(String)方法修改。我們可以在url上或者表單裡新增”_s=your user alias”這樣的形式來指明操作使用者的別名。如果不指明使用者別名,則會認為是預設使用者,可以通過setSessionAliasParamName(null)取消別名功能。

public String getCurrentSessionAlias(HttpServletRequest request) {
        if(sessionParam == null) {
            return DEFAULT_ALIAS;
        }
        String u = request.getParameter(sessionParam);
        if(u == null) {
            return DEFAULT_ALIAS;
        }
        if(!ALIAS_PATTERN.matcher(u).matches()) {
            return DEFAULT_ALIAS;
        }
        return u;
    }

觸發session提交

spring會通過兩個方面確保session提交:

a) response提交,主要包括response的sendRedirect和sendError以及其關聯的位元組字元流的flush和close方法。

abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper {
    public OnCommittedResponseWrapper(HttpServletResponse response) {
        super(response);
    }
    /**
     * Implement the logic for handling the {@link javax.servlet.http.HttpServletResponse} being committed
     */
    protected abstract void onResponseCommitted();

    @Override
    public final void sendError(int sc) throws IOException {
        doOnResponseCommitted();
        super.sendError(sc);
    }
    //sendRedirect處理類似sendError
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new SaveContextServletOutputStream(super.getOutputStream());
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new SaveContextPrintWriter(super.getWriter());
    }

    private void doOnResponseCommitted() {
        if(!disableOnCommitted) {
            onResponseCommitted();
            disableOnResponseCommitted();
        } else if(logger.isDebugEnabled()){
            logger.debug("Skip invoking on");
        }
    }

    private class SaveContextPrintWriter extends PrintWriter {
        private final PrintWriter delegate;

        public SaveContextPrintWriter(PrintWriter delegate) {
            super(delegate);
            this.delegate = delegate;
        }

        public void flush() {
            doOnResponseCommitted();
            delegate.flush();
        }
//close方法與flush方法類似
    }
//SaveContextServletOutputStream處理同字元流
}

onResponseCommitted的實現由子類SessionRepositoryResponseWrapper提供

private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {

        private final SessionRepositoryRequestWrapper request;
        /**
         * @param response the response to be wrapped
         */
        public SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) {
            super(response);
            if(request == null) {
                throw new IllegalArgumentException("request cannot be null");
            }
            this.request = request;
        }

        @Override
        protected void onResponseCommitted() {
            request.commitSession();
        }
    }

response提交後觸發了session提交。
b) SessionRespositoryFilter
僅僅通過response提交時觸發session提交併不能完全保證session的提交,有些情況下不會觸發response提交,比如對相應資源的訪問沒有servlet處理,這種情況就需要通過全域性filter做保證。

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
        //省略
        //filterChain會在所有filter都執行完畢後呼叫對應的servlet
            filterChain.doFilter(strategyRequest, strategyResponse);
        } finally {
        //所有的處理都完成後提交session
            wrappedRequest.commitSession()
        }