为了账号安全,请及时绑定邮箱和手机立即绑定

2018-02-17

标签:
Java

最近要在项目中做用户踢线的功能,由于项目使用spring session来管理用户session,因此特地翻了翻spring session的源码,看看spring session是如何管理的。我们使用redis来存储session,因此本文只对session在redis中的存储结构以及管理做解析。

1 spring session使用
Spring Session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它最好是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequest的getSession()方法时,都会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession。

spring session通过注解@EnableRedisHttpSession或者xml配置

<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
来设置spring session的一些参数,比如session的最大活跃时间(maxInactiveIntervalInSeconds),redis命名空间(redisNamespace),session写入到redis的时机(FlushMode)以及如何序列化写到redis中的session value等等。

要想使用spring session,还需要创建名为springSessionRepositoryFilter的SessionRepositoryFilter类。该类实现了Sevlet Filter接口,当请求穿越sevlet filter链时应该首先经过springSessionRepositoryFilter,这样在后面获取session的时候,得到的将是spring session。为了springSessonRepositoryFilter作为filter链中的第一个,spring session提供了AbstractHttpSessionApplicationInitializer类, 它实现了WebApplicationInitializer类,在onStartup方法中将springSessionRepositoryFilter加入到其他fitler链前面。

public abstract class AbstractHttpSessionApplicationInitializer
        implements WebApplicationInitializer {    /**
     * The default name for Spring Session's repository filter.
     */
    public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter";    public void onStartup(ServletContext servletContext) throws ServletException {
            ......
        insertSessionRepositoryFilter(servletContext);
        afterSessionRepositoryFilter(servletContext);
    }    /**
     * Registers the springSessionRepositoryFilter.
     * @param servletContext the {@link ServletContext}
     */
    private void insertSessionRepositoryFilter(ServletContext servletContext) {
        String filterName = DEFAULT_FILTER_NAME;
             
        DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(
                filterName);
        String contextAttribute = getWebApplicationContextAttribute();        if (contextAttribute != null) {
            springSessionRepositoryFilter.setContextAttribute(contextAttribute);
        }
        registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
    }
}

或者也可以在web.xml里面将springSessionRepositoryFilter加入到filter配置的第一个
该filter最终会把请求代理给具体的一个filter,通过入参的常量可看出它是委派给springSessionRepositoryFilter这样一个具体的filter(由spring容器管理)


5bd553360001f91a05000124.jpg


5bd553370001ae3205000249.jpg

DelegatingFilterProxy.png


查看其父类

public abstract class GenericFilterBean implements
        Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean {    /** Logger available to subclasses */
    protected final Log logger = LogFactory.getLog(getClass());    /**
     * Set of required properties (Strings) that must be supplied as
     * config parameters to this filter.
     */
    private final Set<String> requiredProperties = new HashSet<String>();    private FilterConfig filterConfig;    private String beanName;    private Environment environment = new StandardServletEnvironment();    private ServletContext servletContext;    /**
     * Calls the {@code initFilterBean()} method that might
     * contain custom initialization of a subclass.
     * <p>Only relevant in case of initialization as bean, where the
     * standard {@code init(FilterConfig)} method won't be called.
     * @see #initFilterBean()
     * @see #init(javax.servlet.FilterConfig)
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        initFilterBean();
    }    /**
     * Subclasses can invoke this method to specify that this property
     * (which must match a JavaBean property they expose) is mandatory,
     * and must be supplied as a config parameter. This should be called
     * from the constructor of a subclass.
     * <p>This method is only relevant in case of traditional initialization
     * driven by a FilterConfig instance.
     * @param property name of the required property
     */
    protected final void addRequiredProperty(String property) {        this.requiredProperties.add(property);
    }    /**
     * Standard way of initializing this filter.
     * Map config parameters onto bean properties of this filter, and
     * invoke subclass initialization.
     * @param filterConfig the configuration for this filter
     * @throws ServletException if bean properties are invalid (or required
     * properties are missing), or if subclass initialization fails.
     * @see #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.
        //初始化filter bean
        initFilterBean();        if (logger.isDebugEnabled()) {
            logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
        }
    }    /**
     * Initialize the BeanWrapper for this GenericFilterBean,
     * possibly with custom editors.
     * <p>This default implementation is empty.
     * @param bw the BeanWrapper to initialize
     * @throws BeansException if thrown by BeanWrapper methods
     * @see org.springframework.beans.BeanWrapper#registerCustomEditor
     */
    protected void initBeanWrapper(BeanWrapper bw) throws BeansException {
    }    /**
     * Make the FilterConfig of this filter available, if any.
     * Analogous to GenericServlet's {@code getServletConfig()}.
     * <p>Public to resemble the {@code getFilterConfig()} method
     * of the Servlet Filter version that shipped with WebLogic 6.1.
     * @return the FilterConfig instance, or {@code null} if none available
     * @see javax.servlet.GenericServlet#getServletConfig()
     */
    public final FilterConfig getFilterConfig() {        return this.filterConfig;
    }    /**
     * Make the name of this filter available to subclasses.
     * Analogous to GenericServlet's {@code getServletName()}.
     * <p>Takes the FilterConfig's filter name by default.
     * If initialized as bean in a Spring application context,
     * it falls back to the bean name as defined in the bean factory.
     * @return the filter name, or {@code null} if none available
     * @see javax.servlet.GenericServlet#getServletName()
     * @see javax.servlet.FilterConfig#getFilterName()
     * @see #setBeanName
     */
    protected final String getFilterName() {        return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
    }    /**
     * Make the ServletContext of this filter available to subclasses.
     * Analogous to GenericServlet's {@code getServletContext()}.
     * <p>Takes the FilterConfig's ServletContext by default.
     * If initialized as bean in a Spring application context,
     * it falls back to the ServletContext that the bean factory runs in.
     * @return the ServletContext instance, or {@code null} if none available
     * @see javax.servlet.GenericServlet#getServletContext()
     * @see javax.servlet.FilterConfig#getServletContext()
     * @see #setServletContext
     */
    protected final ServletContext getServletContext() {        return (this.filterConfig != null ? this.filterConfig.getServletContext() : this.servletContext);
    }    /**
     * Subclasses may override this to perform custom initialization.
     * All bean properties of this filter will have been set before this
     * method is invoked.
     */
    protected void initFilterBean() throws ServletException {
    }    /**
     * Subclasses may override this to perform custom filter shutdown.
     * <p>Note: This method will be called from standard filter destruction
     * as well as filter bean destruction in a Spring application context.
     * <p>This default implementation is empty.
     */
    @Override
    public void destroy() {
    }    /**
     * PropertyValues implementation created from FilterConfig init parameters.
     */
    @SuppressWarnings("serial")    private static class FilterConfigPropertyValues extends MutablePropertyValues {        /**
         * Create new FilterConfigPropertyValues.
         */
        public FilterConfigPropertyValues(FilterConfig config, Set<String> requiredProperties)
            throws ServletException {

            Set<String> missingProps = (requiredProperties != null && !requiredProperties.isEmpty()) ?                    new HashSet<String>(requiredProperties) : null;

            Enumeration<?> en = config.getInitParameterNames();            while (en.hasMoreElements()) {
                String property = (String) en.nextElement();
                Object value = config.getInitParameter(property);
                addPropertyValue(new PropertyValue(property, value));                if (missingProps != null) {
                    missingProps.remove(property);
                }
            }            // Fail if we are still missing properties.
            if (missingProps != null && missingProps.size() > 0) {                throw new ServletException(                    "Initialization from FilterConfig for filter '" + config.getFilterName() +                    "' failed; the following required properties were missing: " +
                    StringUtils.collectionToDelimitedString(missingProps, ", "));
            }
        }
    }

}

查看其真正实现方法的子类

public class DelegatingFilterProxy extends GenericFilterBean {    private String contextAttribute;    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());
        }
    }    
    protected boolean isTargetFilterLifecycle() {        return this.targetFilterLifecycle;
    }    @Override
    protected void initFilterBean() throws ServletException {        //同步块,防止spring容器启动时委托的这些filter保证它们的执行顺序
        synchronized (this.delegateMonitor) {            if (this.delegate == null) {                // If no target bean name specified, use filter name.
                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);
                }
            }
        }
    }    @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 registered?");
                    }                    this.delegate = initDelegate(wac);
                }
                delegateToUse = this.delegate;
            }
        }        // Let the delegate perform the actual doFilter operation.
        invokeDelegate(delegateToUse, request, response, filterChain);
    }    @Override
    public void destroy() {
        Filter delegateToUse = this.delegate;        if (delegateToUse != null) {
            destroyDelegate(delegateToUse);
        }
    }    protected WebApplicationContext findWebApplicationContext() {        if (this.webApplicationContext != null) {            // the user has injected a context at construction time -> use it
            if (this.webApplicationContext instanceof ConfigurableApplicationContext) {                if (!((ConfigurableApplicationContext)this.webApplicationContext).isActive()) {                    // the context has not yet been refreshed -> do so before returning it
                    ((ConfigurableApplicationContext)this.webApplicationContext).refresh();
                }
            }            return this.webApplicationContext;
        }
        String attrName = getContextAttribute();        if (attrName != null) {            return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
        }        else {            return WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        }
    }    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }        return delegate;
    }    /**
     * Actually invoke the delegate Filter with the given request and response.
     * 调用了委托的doFilter方法
     */
    protected void invokeDelegate(
            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        delegate.doFilter(request, response, filterChain);
    }    /**
     * Destroy the Filter delegate.
     * Default implementation simply calls {@code Filter.destroy} on it.
     */
    protected void destroyDelegate(Filter delegate) {        if (isTargetFilterLifecycle()) {
            delegate.destroy();
        }
    }
}

查看doFilter真正实现类(session包下)
每次只filter一次

abstract class OncePerRequestFilter implements Filter {    /**
     * Suffix that gets appended to the filter name for the "already filtered" request
     * attribute.
     */
    public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";    private String alreadyFilteredAttributeName = getClass().getName()
            .concat(ALREADY_FILTERED_SUFFIX);    /**
     * This {@code doFilter} implementation stores a request attribute for
     * "already filtered", proceeding without filtering again if the attribute is already
     * there.
     */
    public final void doFilter(ServletRequest request, ServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {        if (!(request instanceof HttpServletRequest)
                || !(response instanceof HttpServletResponse)) {            throw new ServletException(                    "OncePerRequestFilter just supports HTTP requests");
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;        //判断是否已经被Filter
        boolean hasAlreadyFilteredAttribute = request
                .getAttribute(this.alreadyFilteredAttributeName) != null;        if (hasAlreadyFilteredAttribute) {            // Proceed without invoking this filter...
            filterChain.doFilter(request, response);
        }        else {            
            // Do invoke this filter...
            //确实调用此filter
            request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);            try {                //跳至下面的抽象方法
                doFilterInternal(httpRequest, httpResponse, filterChain);
            }            finally {                // Remove the "already filtered" request attribute for this request.
                request.removeAttribute(this.alreadyFilteredAttributeName);
            }
        }
    }    /**
     * Same contract as for {@code doFilter}, but guaranteed to be just invoked once per
     * request within a single request thread.
     * <p>
     * Provides HttpServletRequest and HttpServletResponse arguments instead of the
     * default ServletRequest and ServletResponse ones.
     */
     //唯一实现子类:SessionRepositoryFilter!!!
    protected abstract void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException;    public void init(FilterConfig config) {
    }    public void destroy() {
    }
}

这个类应该是Spring-session里最关键的Bean了,他是一个Filter,他的作用就是封装HttpServietRequest,HttpServletResponse,改变其获取Session的行为,原始的获取Session方式是从服务器容器内获取,而SessionRepositoryFilter将其改变为从其他地方获取,比如从整合的Redis内,当不存在Session时,创建一个封装过的Session,设置到Redis中,同时将此Session关联的Cookie注入到返回结果中,可看其内部的Request和Session的包装类:


5bd5533800010e4805000226.jpg

SessionRepositoryFilter.png

@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";    /**
     * 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.
     */
    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.
     */
    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.
     */
    public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {        if (httpSessionStrategy == null) {            throw new IllegalArgumentException("httpSessionStrategy cannot be null");
        }        this.httpSessionStrategy = httpSessionStrategy;
    }    @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();
        }
    }    public void setServletContext(ServletContext servletContext) {        this.servletContext = servletContext;
    }    /**
     * Allows ensuring that the session is saved if the response is committed.
     * 对现有Request的一个包装类
     */
    private final class SessionRepositoryResponseWrapper
            extends OnCommittedResponseWrapper {        private final SessionRepositoryRequestWrapper request;

        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();
        }
    }    private final class SessionRepositoryRequestWrapper
            extends HttpServletRequestWrapper {        private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class
                .getName();        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;
        }        /**
                 * 更新Session内的数据及最近访问时间到Redis中,若session过期,则清除浏览器cookie的sessionId值
         * Uses the HttpSessionStrategy to write the session id to the response and
         * persist the Session.
         */
        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);
                }
            }
        }        @SuppressWarnings("unchecked")        private HttpSessionWrapper getCurrentSession() {            return (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR);
        }        private void setCurrentSession(HttpSessionWrapper currentSession) {            if (currentSession == null) {
                removeAttribute(this.CURRENT_SESSION_ATTR);
            }            else {
                setAttribute(this.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;
        }        //重写的getSession方法

        //重写获取session的方法,服务区容器内不存在当前请求相关的session,但是请求内含有
        //session=***形式的Cookie时,尝试通过此sessionId从Redis内获取相关的Session信息
        //这就是实现SSO的关键之处

        @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();            //会存到redis中
            session.setLastAccessedTime(System.currentTimeMillis());            //对session也进行了包装(和request的包装同理)
            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);
        }        @Override
        public String getRequestedSessionId() {            return SessionRepositoryFilter.this.httpSessionStrategy
                    .getRequestedSessionId(this);
        }        /**
         * Allows creating an HttpSession from a Session instance.
         */
        private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {

            HttpSessionWrapper(S session, ServletContext servletContext) {                super(session, servletContext);
            }                       //重写session失效方法,在设置Session失效的同时删除Redis数据库内Session信息
            @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;
        }
    }
}

对现有Request的一个包装类


5bd553380001ff9c04290306.jpg

SessionRepositoryRequestWrapper.png

2 创建spring session
RedisSession在创建时设置3个变量creationTime,maxInactiveInterval,lastAccessedTime。maxInactiveInterval默认值为1800,表示1800s之内该session没有被再次使用,则表明该session已过期。每次session被访问都会更新lastAccessedTime的值,session的过期计算公式:当前时间-lastAccessedTime > maxInactiveInterval.

/**

  • Creates a new instance ensuring to mark all of the new attributes to be

  • persisted in the next save operation.
    **/
    RedisSession() {
    this(new MapSession());
    this.delta.put(CREATION_TIME_ATTR, getCreationTime());
    this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
    this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
    this.isNew = true;
    this.flushImmediateIfNecessary();
    }
    public MapSession() {
    this(UUID.randomUUID().toString());
    }
    flushImmediateIfNecessary判断session是否需要立即写入后端存储。

3 获取session
spring session在redis里面保存的数据包括:

SET类型的spring:session:expireations:[min]

min表示从1970年1月1日0点0分经过的分钟数,SET集合的member为expires:[sessionId],表示members会在在min分钟过期。

String类型的spring:session:sessions:expires:[sessionId]

该数据的TTL表示sessionId过期的剩余时间,即maxInactiveInterval。

Hash类型的spring:session:sessions:[sessionId]

session保存的数据,记录了creationTime,maxInactiveInterval,lastAccessedTime,attribute。前两个数据是用于session过期管理的辅助数据结构。

应用通过getSession(boolean create)方法来获取session数据,参数create表示session不存在时是否创建新的session。getSession方法首先从请求的“.CURRENT_SESSION”属性来获取currentSession,没有currentSession,则从request取出sessionId,然后读取spring:session:sessions:[sessionId]的值,同时根据lastAccessedTime和MaxInactiveIntervalInSeconds来判断这个session是否过期。如果request中没有sessionId,说明该用户是第一次访问,会根据不同的实现,如RedisSession,MongoExpiringSession,GemFireSession等来创建一个新的session。

另, 从request取sessionId依赖具体的HttpSessionStrategy的实现,spring session给了两个默认的实现CookieHttpSessionStrategy和HeaderHttpSessionStrategy,即从cookie和header中取出sessionId。

@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
// 从request请求中得到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
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
if (!create) {
return null;
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(System.currentTimeMillis());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
spring session为什么会使用3个key,而不是一个key?接下来回答。

4 session有效期与删除
spring session的有效期指的是访问有效期,每一次访问都会更新lastAccessedTime的值,过期时间为lastAccessedTime + maxInactiveInterval,也即在有效期内每访问一次,有效期就向后延长maxInactiveInterval。

对于过期数据,一般有三种删除策略:

1)定时删除,即在设置键的过期时间的同时,创建一个定时器, 当键的过期时间到来时,立即删除。

2)惰性删除,即在访问键的时候,判断键是否过期,过期则删除,否则返回该键值。

3)定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

redis删除过期数据采用的是懒性删除+定期删除组合策略,也就是数据过期了并不会及时被删除。为了实现session过期的及时性,spring session采用了定时删除的策略,但它并不是如上描述在设置键的同时设置定时器,而是采用固定频率(1分钟)轮询删除过期值,这里的删除是惰性删除。

轮询操作并没有去扫描所有的spring:session:sessions:[sessionId]的过期时间,而是在当前分钟数检查前一分钟应该过期的数据,即spring:session:expirations:[min]的members,然后delete掉spring:session:expirations:[min],惰性删除spring:session:sessions:expires:[sessionId]。

还有一点是,查看三个数据结构的TTL时间,spring:session:sessions:[sessionId]和spring:session:expirations:[min]比真正的有效期大5分钟,目的是确保当expire key数据过期后,监听事件还能获取到session保存的原始数据。

@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);
// preMin时间到,将spring:session:expirations:[min], set集合中members包括了这一分钟之内需要过期的所有
// expire key删掉, member元素为expires:[sessionId]
String expirationKey = getExpirationKey(prevMin);
Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
this.redis.delete(expirationKey);
for (Object session : sessionsToExpire) {
// sessionKey为spring:session:sessions:expires:[sessionId]
String sessionKey = getSessionKey((String) session);
//利用redis的惰性删除策略
touch(sessionKey);
}
}
spring session在redis中保存了三个key,为什么? sessions key记录session本身的数据,expires key标记session的准确过期时间,expiration key保证session能够被及时删除,spring监听事件能够被及时处理。

上面的代码展示了session expires key如何被删除,那session每次都是怎样更新过期时间的呢? 每一次http请求,在经过所有的filter处理过后,spring session都会通过onExpirationUpdated()方法来更新session的过期时间, 具体的操作看下面源码的注释。

public void onExpirationUpdated(Long originalExpirationTimeInMilli,
ExpiringSession session) {
String keyToExpire = "expires:" + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
if (originalExpirationTimeInMilli != null) {
long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
// 更新expirations:[min],两个分钟数之内都有这个session,将前一个set中的成员删除
if (toExpire != originalRoundedUp) {
String expireKey = getExpirationKey(originalRoundedUp);
this.redis.boundSetOps(expireKey).remove(keyToExpire);
}
}
long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds();
String sessionKey = getSessionKey(keyToExpire);
if (sessionExpireInSeconds < 0) {
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).persist();
this.redis.boundHashOps(getSessionKey(session.getId())).persist();
return;
}
String expireKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis
.boundSetOps(expireKey);
expireOperations.add(keyToExpire);
long fiveMinutesAfterExpires = sessionExpireInSeconds
+ TimeUnit.MINUTES.toSeconds(5);
// expirations:[min] key的过期时间加5分钟
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
if (sessionExpireInSeconds == 0) {
this.redis.delete(sessionKey);
}
else {
// expires:[sessionId] 值为“”,过期时间为MaxInactiveIntervalInSeconds
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
TimeUnit.SECONDS);
}
// sessions:[sessionId]的过期时间 加5分钟
this.redis.boundHashOps(getSessionKey(session.getId()))
.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}



作者:芥末无疆sss
链接:https://www.jianshu.com/p/6c5918a22adc
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。




点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消