【转载】spring-session负载均衡原理分析

两盒软妹~` 提交于 2019-11-29 09:18:26

注明转载:https://www.jianshu.com/p/beaf18704c3c

第一部分:我会用循序渐进的方式来展示源码,从大家最熟悉的地方入手,而不是直接从系统启动来debug源码。直接debug源码看到后来大家都会一头雾水。 本文先从request.getSession()开始剖析源码,目标是让读者清楚的知晓Spring-session的产生过程。

第二部分:再上一部分Spring-session的产生过程的研究中如果读者清楚了整个过程的脉络,那么肯定会产生一些疑惑:Servlet容器如何从默认的Session切换到Spring-session?为什么request.getSession()会直接调用Spring的session管理方案?这一块研究结束后整个Spring-session的大体原理分析就结束了。

剩下的就是其他一些策略的问题,篇幅有限,不再展开。读者可以私下研究或者评论区域我们讨论。比如

1.CookieHttpSessionStrategy和HeaderHttpSessionStrategy的区别
2.Session创建成功后存储到session仓库的具体过程?
...

那么,先从第一部分开始

一. 提出问题假设

Spring-Session 的思路是替换Servlet容器提供的HttpSession。在web程序中通过调用方法 request.getSession() 生成session。Servlet容器里面默认的request实现是HttpServletRequestWrapper类。那么为了替换原始的HttpSession,Spring-Session有两种方案来重写getSession()方法 :

1.实现`HttpServletRequest`接口
2.继承`HttpServletRequestWrapper`类

我们从springmvc的controller进入request.getSession()方法,debug进去后发现getSession方法在这个类SessionRepositoryRequestWrapper,并且这个类继承了HttpServletRequestWrapper。很开心有木有?验证了我们上面的想法Spring-Session用第2种继承的方式来实现HttpSession的自定义。

/*IndexController.java*/
    @Resource
    HttpServletRequest request;

    @RequestMapping({ "", "/index" })
    public String index(Model model) {
        HttpSession session = request.getSession();  //方法debug跟踪
        Object user = session.getAttribute("curuser");
        if(user == null) return "redirect:login";
        model.addAttribute("port", request.getLocalPort());
        return "index";
    }
/*SessionRepositoryRequestWrapper.java*/

    @Override
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }

大概的思路了然,那么getSession(true)到底是如何运作的呢?getSession()这里的业务也是最复杂的,存在各种状态的判断。开始研究getSession()。

二.在Controller中获取Session

在controller中通过request.getSession()来获取Session,下图是此方法执行的过程。

 

image.png

@Override
public HttpSessionWrapper getSession(boolean create) {
    /*
    从request中获取Session,首次访问返回null
    其实这里相当于request.getAttribute(key);
    在Session创建成功后会调用request.setAttribute(key,session);
    以便于在同一个request请求中直接获取session
    */
    HttpSessionWrapper currentSession = getCurrentSession();
    if (currentSession != null) {
        return currentSession;
    }
    /*
    从Cookie或者header中获取SESSIONID,如果我们用Cookie策略,这也是spring-session默认的。
    可以查看浏览器cookie。存在键值对 SESSION:XXXXXXXXXXXXXXXX
    */
    String requestedSessionId = getRequestedSessionId();
    if (requestedSessionId != null
            && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
        /*
          根据上文得到的sessionid从Session仓库中获取Session
        */
        S session = getSession(requestedSessionId);
        if (session != null) {//有效的Session
            this.requestedSessionIdValid = true;
            currentSession = new HttpSessionWrapper(session, getServletContext());
            currentSession.setNew(false);
            setCurrentSession(currentSession);
            return currentSession;
        }else {//无效的session,
        
            if (SESSION_LOGGER.isDebugEnabled()) {
                SESSION_LOGGER.debug(
                "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
            }
            //Session无效,在request中增加一个键值对
            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());
    currentSession = new HttpSessionWrapper(session, getServletContext());

    //将刚创建的session加入到request,以便于本次请求中再次getSession()时直接返回。
    setCurrentSession(currentSession);

    return currentSession;
}

至此,我们在controller中获取到了Session。可以存取数据到Session里面。在controller层response的时候把Session存储到Session仓库中(redis、mongo等)

三.spring-session与session是如何做到无缝切换的

web容器实现session共享的插件也有,比如tomcat-redis-session-manager等,缺点比较多:需要在tomcat做配置,侵入严重。
Spring-session用了一个比较聪明又简单的办法

1.自定义一个Filter ,springSessionRepositoryFilter,拦截所有请求
2.继承HttpServletRequestWrapper等类,重写getSession()等方法。

这里我们看看Spring官方文档

we can create our Spring configuration. The Spring configuration is responsible for creating a Servlet Filter that replaces the HttpSession implementation with an implementation backed by Spring Session. Add the following Spring Configuration:
(我们可以创建一个Spring 的配置,这个文件是用来创建一个Filter,这个Filter里面可以实现Spring session替换HttpSession的功能。Spring的配置如下)

XML实现方式

<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>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

DelegatingFilterProxy这个类拦截每次请求,并且寻找到springSessionRepositoryFilter这个bean,并且将它转换成Filter,用这个Filter处理每个request请求。

获取springSessionRepositoryFilter这个bean。
Object obj = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()).getBean("springSessionRepositoryFilter");

debug查看对象obj ,没错这就是spring-session最核心的Filter ——SessionReponsitoryFilter
org.springframework.session.web.http.SessionRepositoryFilter@228204ee

spring-session重写的request(SessionRepositoryRequestWrapper),response(SessionRepositoryResponseWrapper)和Session(HttpSessionWrapper)都是SessionReponsitoryFilter类的内部类。第一部分着重说的getSession(boolean)方法就是在SessionRepositoryRequestWrapper这个类里面重写的。

注解实现方式

//@EnableRedisHttpSession这个注解创建了springSessionRepositoryFilter的Bean。
//并且创建了一个操作Redis的RedisConnectionFactory工厂类

@EnableRedisHttpSession 
public class Config {
        @Bean
        public LettuceConnectionFactory connectionFactory() {
                return new LettuceConnectionFactory(); 
        }
}

上面Config创建了Filter,接下来需要将这个Config加载到Spring。以此来实现每次请求过来首先经过这个Filter。

public class Initializer extends AbstractHttpSessionApplicationInitializer { 

        public Initializer() {
                super(Config.class); 
        }
}

那么上面两种配置方式里的这个SessionReponsitoryFilter到底是啥样的?这个Filter才是Spring-session的核心。我们来看看
SessionReponsitoryFilter 源代码

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
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);

    /**
     * The session repository request attribute name.
     */
    public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class
            .getName();

    /**
     * Invalid session id (not backed by the session repository) request attribute name.
     */
    public static final String INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR
            + ".invalidSessionId";

    private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR
            + ".CURRENT_SESSION";

    /**
     * The default filter order.
     */
    public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;

    private final SessionRepository<S> sessionRepository;

    private ServletContext servletContext;

    private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();

    /**
     * Creates a new instance.
     *
     * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null.
     */
    public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        if (sessionRepository == null) {
            throw new IllegalArgumentException("sessionRepository cannot be null");
        }
        this.sessionRepository = sessionRepository;
    }

    /**
     * Sets the {@link HttpSessionStrategy} to be used. The default is a
     * {@link CookieHttpSessionStrategy}.
     *
     * @param httpSessionStrategy the {@link HttpSessionStrategy} to use. Cannot be null.
     设置HttpSessionStrategy的策略,默认策略是CookieHttpSessionStrategy。表示从cookie中获取sessionid。
     */
    public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
        if (httpSessionStrategy == null) {
            throw new IllegalArgumentException("httpSessionStrategy cannot be null");
        }
        this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(
                httpSessionStrategy);
    }

    /**
     * Sets the {@link MultiHttpSessionStrategy} to be used. The default is a
     * {@link CookieHttpSessionStrategy}.
     *
     * @param httpSessionStrategy the {@link MultiHttpSessionStrategy} to use. Cannot be
     * null.
     */
    public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
        if (httpSessionStrategy == null) {
            throw new IllegalArgumentException("httpSessionStrategy cannot be null");
        }
        this.httpSessionStrategy = httpSessionStrategy;
    }
    /**
   这个方法是典型的模板方法设计模式的运用;SessionRepositoryFilter的父类定义了抽象方法doFilterInternal,并且在doFilter中调用,具体的实现丢给子类。

    */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        //封装request和response
        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);
        //这里的作用是通过方法request.setAttribute(HttpSessionManager.class.getName(), 策略);
        //把CookieHttpSessionStrategy加入到request。下面的response一样
        HttpServletRequest strategyRequest = this.httpSessionStrategy
                .wrapRequest(wrappedRequest, wrappedResponse);
        HttpServletResponse strategyResponse = this.httpSessionStrategy
                .wrapResponse(wrappedRequest, wrappedResponse);

        try {
            filterChain.doFilter(strategyRequest, strategyResponse);
        }
        finally {
                        //这里是response的时候把session加入到session仓库(redis,MongoDB等),该方法在下面的SessionRepositoryRequestWrapper类
            wrappedRequest.commitSession();
        }
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * Allows ensuring that the session is saved if the response is committed.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryResponseWrapper
            extends OnCommittedResponseWrapper {

        private final SessionRepositoryRequestWrapper request;

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

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

    /**
     * A {@link javax.servlet.http.HttpServletRequest} that retrieves the
     * {@link javax.servlet.http.HttpSession} using a
     * {@link org.springframework.session.SessionRepository}.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryRequestWrapper
            extends HttpServletRequestWrapper {
        private Boolean requestedSessionIdValid;
        private boolean requestedSessionInvalidated;
        private final HttpServletResponse response;
        private final ServletContext servletContext;

        private SessionRepositoryRequestWrapper(HttpServletRequest request,
                HttpServletResponse response, ServletContext servletContext) {
            super(request);
            this.response = response;
            this.servletContext = servletContext;
        }

        /**
         * Uses the HttpSessionStrategy to write the session id to the response and
         * persist the Session.
         *  将session加入到session仓库(redis,MongoDB等
         */
        private void commitSession() {
            HttpSessionWrapper wrappedSession = getCurrentSession();
            if (wrappedSession == null) {
                if (isInvalidateClientSession()) {
                    SessionRepositoryFilter.this.httpSessionStrategy
                            .onInvalidateSession(this, this.response);
                }
            }
            else {
                S session = wrappedSession.getSession();
                SessionRepositoryFilter.this.sessionRepository.save(session);
                if (!isRequestedSessionIdValid()
                        || !session.getId().equals(getRequestedSessionId())) {
                    SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
                            this, this.response);
                }
            }
        }
      //从当前request中获取session
        @SuppressWarnings("unchecked")
        private HttpSessionWrapper getCurrentSession() {
            return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
        }
      //将session存储到当前request请求中
        private void setCurrentSession(HttpSessionWrapper currentSession) {
            if (currentSession == null) {
                removeAttribute(CURRENT_SESSION_ATTR);
            }
            else {
                setAttribute(CURRENT_SESSION_ATTR, currentSession);
            }
        }

        @SuppressWarnings("unused")
        public String changeSessionId() {
            HttpSession session = getSession(false);

            if (session == null) {
                throw new IllegalStateException(
                        "Cannot change session ID. There is no session associated with this request.");
            }

            // eagerly get session attributes in case implementation lazily loads them
            Map<String, Object> attrs = new HashMap<String, Object>();
            Enumeration<String> iAttrNames = session.getAttributeNames();
            while (iAttrNames.hasMoreElements()) {
                String attrName = iAttrNames.nextElement();
                Object value = session.getAttribute(attrName);

                attrs.put(attrName, value);
            }

            SessionRepositoryFilter.this.sessionRepository.delete(session.getId());
            HttpSessionWrapper original = getCurrentSession();
            setCurrentSession(null);

            HttpSessionWrapper newSession = getSession();
            original.setSession(newSession.getSession());

            newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
            for (Map.Entry<String, Object> attr : attrs.entrySet()) {
                String attrName = attr.getKey();
                Object attrValue = attr.getValue();
                newSession.setAttribute(attrName, attrValue);
            }
            return newSession.getId();
        }
  
        @Override
        public boolean isRequestedSessionIdValid() {
            if (this.requestedSessionIdValid == null) {
                String sessionId = getRequestedSessionId();
                S session = sessionId == null ? null : getSession(sessionId);
                return isRequestedSessionIdValid(session);
            }

            return this.requestedSessionIdValid;
        }

        private boolean isRequestedSessionIdValid(S session) {
            if (this.requestedSessionIdValid == null) {
                this.requestedSessionIdValid = session != null;
            }
            return this.requestedSessionIdValid;
        }

        private boolean isInvalidateClientSession() {
            return getCurrentSession() == null && this.requestedSessionInvalidated;
        }

        private S getSession(String sessionId) {
            S session = SessionRepositoryFilter.this.sessionRepository
                    .getSession(sessionId);
            if (session == null) {
                return null;
            }
            session.setLastAccessedTime(System.currentTimeMillis());
            return session;
        }

        @Override
        public HttpSessionWrapper getSession(boolean create) {
            HttpSessionWrapper currentSession = getCurrentSession();
            if (currentSession != null) {
                return currentSession;
            }
            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)"));
            }
            S session = SessionRepositoryFilter.this.sessionRepository.createSession();
            session.setLastAccessedTime(System.currentTimeMillis());
            currentSession = new HttpSessionWrapper(session, getServletContext());
            setCurrentSession(currentSession);
            return currentSession;
        }

        @Override
        public ServletContext getServletContext() {
            if (this.servletContext != null) {
                return this.servletContext;
            }
            // Servlet 3.0+
            return super.getServletContext();
        }

        @Override
        public HttpSessionWrapper getSession() {
            return getSession(true);
        }
        //从session策略中获取sessionid
        @Override
        public String getRequestedSessionId() {
            return SessionRepositoryFilter.this.httpSessionStrategy
                    .getRequestedSessionId(this);
        }

        /**
         * Allows creating an HttpSession from a Session instance.
         *
         * @author Rob Winch
         * @since 1.0
         */
        private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {

            HttpSessionWrapper(S session, ServletContext servletContext) {
                super(session, servletContext);
            }

            @Override
            public void invalidate() {
                super.invalidate();
                SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                setCurrentSession(null);
                SessionRepositoryFilter.this.sessionRepository.delete(getId());
            }
        }
    }

    /**
     * A delegating implementation of {@link MultiHttpSessionStrategy}.
     */
    static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {
        private HttpSessionStrategy delegate;

        /**
         * Create a new {@link MultiHttpSessionStrategyAdapter} instance.
         * @param delegate the delegate HTTP session strategy
         */
        MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {
            this.delegate = delegate;
        }

        public String getRequestedSessionId(HttpServletRequest request) {
            return this.delegate.getRequestedSessionId(request);
        }

        public void onNewSession(Session session, HttpServletRequest request,
                HttpServletResponse response) {
            this.delegate.onNewSession(session, request, response);
        }

        public void onInvalidateSession(HttpServletRequest request,
                HttpServletResponse response) {
            this.delegate.onInvalidateSession(request, response);
        }

        public HttpServletRequest wrapRequest(HttpServletRequest request,
                HttpServletResponse response) {
            return request;
        }

        public HttpServletResponse wrapResponse(HttpServletRequest request,
                HttpServletResponse response) {
            return response;
        }
    }
}

结语

spring-session源码的解读就这么粗糙的结束了,一些状态判断性的源码没有解读。我相信只要读者把主线业务整理明白了,其他方法小菜一碟。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!