最近要在项目中做用户踢线的功能,由于项目使用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容器管理)
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的包装类:
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的一个包装类
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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
共同学习,写下你的评论
评论加载中...
作者其他优质文章