最近要在项目中做用户踢线的功能,由于项目使用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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章



