Spring Session 內部實現原理(源碼分析)

Spring Session的架構

Spring Session定義了一組標準的接口,可以通過實現這些接口間接訪問底層的數據存儲。Spring Session定義了如下核心接口:Session、ExpiringSession以及SessionRepository,針對不同的數據存儲,它們需要分別實現。

  • org.springframework.session.Session接口定義了session的基本功能,如設置和移除屬性。這個接口并不關心底層技術,因此能夠比servlet HttpSession適用于更為廣泛的場景中。
  • org.springframework.session.ExpiringSession擴展了Session接口,它提供了判斷session是否過期的屬性。RedisSession是這個接口的一個樣例實現。
  • org.springframework.session.SessionRepository定義了創建、保存、刪除以及檢索session的方法。將Session實例真正保存到數據存儲的邏輯是在這個接口的實現中編碼完成的。例如,RedisOperationsSessionRepository就是這個接口的一個實現,它會在Redis中創建、存儲和刪除session。

在請求/響應周期中,客戶端和服務器之間需要協商同意一種傳遞session id的方式。例如,如果請求是通過HTTP傳遞進來的,那么session可以通過HTTP cookie或HTTP Header信息與請求進行關聯。

對于HTTP協議來說,Spring Session定義了HttpSessionStrategy接口以及兩個默認實現,即CookieHttpSessionStrategy和HeaderHttpSessionStrategy,其中前者使用HTTP cookie將請求與session id關聯,而后者使用HTTP header將請求與session關聯。

核心思想:
通過 org.springframework.session.web.http.SessionRepositoryFilterdoFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)對所有的請求進行攔截,使用包裝(Wrapper)或者說是裝飾(Decorator)模式對 request, response進行包裝并重寫HttpServletRequest 的 getSession方法,然后通過 filterChain向后傳遞。

** 本文基于 Spring Session 1.3.0.RELEASE 源代碼分析**



首先,看看我們在web.xml中配置:

<!-- spring 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>

DelegatingFilterProxy

DelegatingFilterProxy 顧名思義是一個Filter的代理類,其代碼如下:


public class DelegatingFilterProxy extends GenericFilterBean {

    private WebApplicationContext webApplicationContext;

    private String targetBeanName;

    private boolean targetFilterLifecycle = false;

    private volatile Filter delegate;

    private final Object delegateMonitor = new Object();

    public DelegatingFilterProxy() {
    }

    public DelegatingFilterProxy(Filter delegate) {
        Assert.notNull(delegate, "delegate Filter object must not be null");
        this.delegate = delegate;
    }

    public DelegatingFilterProxy(String targetBeanName) {
        this(targetBeanName, null);
    }

    public DelegatingFilterProxy(String targetBeanName, WebApplicationContext wac) {
        Assert.hasText(targetBeanName, "target Filter bean name must not be null or empty");
        this.setTargetBeanName(targetBeanName);
        this.webApplicationContext = wac;
        if (wac != null) {
            this.setEnvironment(wac.getEnvironment());
        }
    }
}

DelegatingFilterProxy 繼承自 GenericFilterBean,GenericFilterBean是一個抽象類,分別實現了 Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean接口,繼承關系如下圖:

DelegatingFilterProxy.png

GenericFilterBean 主要代碼如下:

public abstract class GenericFilterBean implements
        Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws ServletException {
        initFilterBean();
    }

    @Override
    public final void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");
        }

        this.filterConfig = filterConfig;

        // Set bean properties from init parameters.
        try {
            PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            String msg = "Failed to set bean properties on filter '" +
                filterConfig.getFilterName() + "': " + ex.getMessage();
            logger.error(msg, ex);
            throw new NestedServletException(msg, ex);
        }

        // Let subclasses do whatever initialization they like.
        initFilterBean();

        if (logger.isDebugEnabled()) {
            logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
        }
    }

    protected void initFilterBean() throws ServletException {
    }
}

由此可見,當DelegatingFilterProxy 在執行Filter的 init 方法時,會調用 initFilterBean方法,如下:


    /**
     * Spring容器啟動時初始化Filter
     */
    @Override
    protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // 如果targetBeanName為null,則使用當前Filter的名稱
                if (this.targetBeanName == null) {
                    this.targetBeanName = getFilterName();
                }
                // Fetch Spring root application context and initialize the delegate early,
                // if possible. If the root application context will be started after this
                // filter proxy, we'll have to resort to lazy initialization.
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

getFilterName方法繼承自GenericFilterBean ,如下:


    public final FilterConfig getFilterConfig() {
        return this.filterConfig;
    }

    protected final String getFilterName() {
        return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
    }

首先,會獲取targetBeanName 的值,這里會取當前Filter的名稱,也即我們在web.xml中配置的 <filter-name>屬性值:springSessionRepositoryFilter,然后調用 initDelegate方法為 delegate賦值,initDelegate方法如下:

/**
     * 初始化代理Filter
     */
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        //根據getTargetBeanName() 即 springSessionRepositoryFilter去WebApplicationContext查找bean
        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
        if (isTargetFilterLifecycle()) {
            //調用代理Filter的init方法
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

這里 根據 springSessionRepositoryFilter去WebApplicationContext查找Bean,找到的Filter究竟是誰呢?

還記得我們在 applicationContext.xml中配置的 第一個bean嗎?

<!--spring session-->
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <property name="maxInactiveIntervalInSeconds" value="1800"></property>
    </bean>

RedisHttpSessionConfiguration的繼承關系如下:

RedisHttpSessionConfiguration.png

Spring Session為了減輕我們配置Bean 的負擔,在 RedisHttpSessionConfiguration以及它的父類 SpringHttpSessionConfiguration中 自動生成了許多Bean,我們看看 SpringHttpSessionConfiguration的源碼:

@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {

    private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();

    private boolean usesSpringSessionRememberMeServices;

    private ServletContext servletContext;

    private CookieSerializer cookieSerializer;

    private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy;

    private List<HttpSessionListener> httpSessionListeners = new ArrayList<HttpSessionListener>();

    /**
     * 我們在web.xml配置的Filter名稱
     */
    @Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(
                sessionRepository);
        sessionRepositoryFilter.setServletContext(this.servletContext);
        if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
            sessionRepositoryFilter.setHttpSessionStrategy(
                    (MultiHttpSessionStrategy) this.httpSessionStrategy);
        }
        else {
            sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
        }
        return sessionRepositoryFilter;
    }
}

看到這里明白了吧,根據 springSessionRepositoryFilter從 WebApplicationContext取到的是 org.springframework.session.web.http.SessionRepositoryFilter對象。

接下來,我們來看看 DelegatingFilterProxy 的doFilter方法:

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // Lazily initialize the delegate if necessary.
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    this.delegate = initDelegate(wac);
                }
                delegateToUse = this.delegate;
            }
        }

        // Let the delegate perform the actual doFilter operation.
        invokeDelegate(delegateToUse, request, response, filterChain);
    }

    /**
     * 調用代理Filter
     */
    protected void invokeDelegate(
            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        delegate.doFilter(request, response, filterChain);
    }

DelegatingFilterProxy doFilter方法將每次請求都交給 delegate處理,即交給 org.springframework.session.web.http.SessionRepositoryFilter 進行處理。

SessionRepositoryFilter

Spring Session對HTTP的支持所依靠的是一個簡單老式的Servlet Filter,借助servlet規范中標準的特性來實現Spring Session的功能。SessionRepositoryFilter就是 Servlet Filter的一個標準實現,代碼如下:


public class SessionRepositoryFilter<S extends ExpiringSession>
        extends OncePerRequestFilter {
    private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class
            .getName().concat(".SESSION_LOGGER");

    private static final Log SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME);

    private final SessionRepository<S> sessionRepository;

    private ServletContext servletContext;

    private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();

    /**
     * 構造方法
     */
    public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        if (sessionRepository == null) {
            throw new IllegalArgumentException("sessionRepository cannot be null");
        }
        this.sessionRepository = sessionRepository;
    }

    /**
     * doFilter方法調用
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);

        HttpServletRequest strategyRequest = this.httpSessionStrategy
                .wrapRequest(wrappedRequest, wrappedResponse);
        HttpServletResponse strategyResponse = this.httpSessionStrategy
                .wrapResponse(wrappedRequest, wrappedResponse);

        try {
            filterChain.doFilter(strategyRequest, strategyResponse);
        }
        finally {
            wrappedRequest.commitSession();
        }
    }
}

SessionRepositoryFilter 繼承自OncePerRequestFilter,也是一個標準的Servlet Filter。真正的核心在于它對請求的HttpServletRequest ,HttpServletResponse 對進行包裝了之后,然后調用 filterChain.doFilter(strategyRequest, strategyResponse); 往后傳遞,后面調用者通過 HttpServletRequest.getSession();獲得session的話,得到的將會是Spring Session 提供的 HttpServletSession實例。

其中,SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper是 SessionRepositoryFilter中的 內部類。

我們可以在Controller 中進行 debug 看看我們拿到的 HttpServletRequest 和 HttpSession 到底是什么?

@RestController
public class EchoController {

    @RequestMapping(value = "/query", method = RequestMethod.GET)
    public User query(String name, HttpServletRequest request, HttpSession session){
        
        System.out.println(session);

        User user = new User();
        user.setId(15L);
        user.setName(name);
        user.setPassword("root");
        user.setAge(28);

        session.setAttribute("user", user);

        return user;
    }
}

在IDEA中 單步調試,結果如下:


debug.png

接下來,我們分析一下 SessionRepositoryRequestWrapper 中關于 getSession()實現:


    /**
     * HttpServletRequest getSession()實現
     */
    @Override
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }
    
    @Override
    public HttpSessionWrapper getSession(boolean create) {
        HttpSessionWrapper currentSession = getCurrentSession();
        if (currentSession != null) {
            return currentSession;
        }
        //從當前請求獲取sessionId
        String requestedSessionId = getRequestedSessionId();
        if (requestedSessionId != null
                && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
            S session = getSession(requestedSessionId);
            if (session != null) {
                this.requestedSessionIdValid = true;
                currentSession = new HttpSessionWrapper(session, getServletContext());
                currentSession.setNew(false);
                setCurrentSession(currentSession);
                return currentSession;
            }
            else {
                // This is an invalid session id. No need to ask again if
                // request.getSession is invoked for the duration of this request
                if (SESSION_LOGGER.isDebugEnabled()) {
                    SESSION_LOGGER.debug(
                            "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
                }
                setAttribute(INVALID_SESSION_ID_ATTR, "true");
            }
        }
        if (!create) {
            return null;
        }
        if (SESSION_LOGGER.isDebugEnabled()) {
            SESSION_LOGGER.debug(
                    "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
                            + SESSION_LOGGER_NAME,
                    new RuntimeException(
                            "For debugging purposes only (not an error)"));
        }
        //為當前請求創建session
        S session = SessionRepositoryFilter.this.sessionRepository.createSession();
        session.setLastAccessedTime(System.currentTimeMillis());
        //對Spring session 進行包裝(包裝成HttpSession)
        currentSession = new HttpSessionWrapper(session, getServletContext());
        setCurrentSession(currentSession);
        return currentSession;
    }
    
    /**
     * 根據sessionId獲取session
     */
    private S getSession(String sessionId) {
        S session = SessionRepositoryFilter.this.sessionRepository
                .getSession(sessionId);
        if (session == null) {
            return null;
        }
        session.setLastAccessedTime(System.currentTimeMillis());
        return session;
    }
        
    /**
     * 從當前請求獲取sessionId
     */
    @Override
    public String getRequestedSessionId() {
        return SessionRepositoryFilter.this.httpSessionStrategy
                .getRequestedSessionId(this);
    }
    
    private void setCurrentSession(HttpSessionWrapper currentSession) {
        if (currentSession == null) {
            removeAttribute(CURRENT_SESSION_ATTR);
        }
        else {
            setAttribute(CURRENT_SESSION_ATTR, currentSession);
        }
    }
    /**
     * 獲取當前請求session
     */
    @SuppressWarnings("unchecked")
    private HttpSessionWrapper getCurrentSession() {
        return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
    }

注釋寫的很清楚,就不啰嗦了。

參考資料

通過Spring Session實現新一代的Session管理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,825評論 18 139
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,922評論 6 342
  • 這部分主要是與Java Web和Web Service相關的面試題。 96、闡述Servlet和CGI的區別? 答...
    雜貨鋪老板閱讀 1,423評論 0 10
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,332評論 11 349
  • 本文包括:1、Filter簡介2、Filter是如何實現攔截的?3、Filter開發入門4、Filter的生命周期...
    廖少少閱讀 7,303評論 3 56