主要参考链接:
通过Spring Session实现新一代的Session管理
如何区分不同的用户
spring-session简介、使用及实现原理
sessionid如何产生?由谁产生?保存在哪里?
学习Spring-Session+Redis实现session共享
利用nginx实现负载均衡
主要是看了腾讯课堂上的动脑学院的2017-12-15解决session一致性问题后对于spring-session的简单的了解,对于一些基本的概念有些模糊,慢慢的去了解,必然会有所收益。
//maven依赖
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
<version>1.2.1.RELEASEversion>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.8.1version>
dependency>
//mvc.xml
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="600"/>
bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="10" />
bean>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="hostName" value="${redis_hostname}"/>
<property name="port" value="${redis_port}"/>
<property name="password" value="${redis_pwd}" />
<property name="timeout" value="3000"/>
<property name="usePool" value="true"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
bean>
//web.xml spring session基于拦截器处理
<filter>
<filter-name>springSessionRepositoryFilterfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
@Bean 放置在方法之上,方法的名称就相当于之前我们定义在xml中的id
@Service
public class BeanTest {
@Bean
public BeanTest getBean(){
BeanTest bean = new BeanTest();
System.out.println("调用方法:"+bean);
return bean;
}
}
public class Main {
@SuppressWarnings("unused")
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
Object bean1 = context.getBean("getBean");
System.out.println(bean1);
Object bean2 = context.getBean("getBean");
System.out.println(bean2);
}
}
注解@import配合@Configuration,放置在注解上,可以通过实现importAware接口
通过注释可以知道,我们可以通过在Import的类中读取到注解上的相关的信息。
/**
* Interface to be implemented by any @{@link Configuration} class that wishes
* to be injected with the {@link AnnotationMetadata} of the @{@code Configuration}
* class that imported it. Useful in conjunction with annotations that
* use @{@link Import} as a meta-annotation.
*
* @author Chris Beams
* @since 3.1
*/
public interface ImportAware extends Aware {
/**
* Set the annotation metadata of the importing @{@code Configuration} class.
*/
void setImportMetadata(AnnotationMetadata importMetadata);
}
@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);
}
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
int maxInactiveIntervalInSeconds() default 1800;
/**
*
* Defines a unique namespace for keys. The value is used to isolate sessions by
* changing the prefix from "spring:session:" to
* "spring:session:<redisNamespace>:". The default is "" such that all Redis
* keys begin with "spring:session".
*
*
*
* For example, if you had an application named "Application A" that needed to keep
* the sessions isolated from "Application B" you could set two different values for
* the applications and they could function within the same Redis instance.
*
*
* @return the unique namespace for keys
*/
String redisNamespace() default "";
/**
*
* Sets the flush mode for the Redis sessions. The default is ON_SAVE which only
* updates the backing Redis when
* {@link SessionRepository#save(org.springframework.session.Session)} is invoked. In
* a web environment this happens just before the HTTP response is committed.
*
*
* Setting the value to IMMEDIATE will ensure that the any updates to the Session are
* immediately written to the Redis instance.
*
*
* @return the {@link RedisFlushMode} to use
* @since 1.1
*/
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
}
这个就是RedisHttpSessionConfiguration中对于importAware的实现,获取对应的注解的值,如果没有通过注解实现这个,不会去自动的调用这个方法的!
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map enableAttrMap = importMetadata
.getAnnotationAttributes(EnableRedisHttpSession.class.getName());
AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap);
this.maxInactiveIntervalInSeconds = enableAttrs
.getNumber("maxInactiveIntervalInSeconds");
this.redisNamespace = enableAttrs.getString("redisNamespace");
this.redisFlushMode = enableAttrs.getEnum("redisFlushMode");
}
@Bean
public SessionRepositoryFilter extends ExpiringSession> springSessionRepositoryFilter(
SessionRepository sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
sessionRepositoryFilter.setHttpSessionStrategy(
(MultiHttpSessionStrategy) this.httpSessionStrategy);
}
else {
sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
}
return sessionRepositoryFilter;
}
完整源码,还有监听seesion的变化List可以自己手动的配置
public class SpringHttpSessionConfiguration {
private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();
private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy;
private List httpSessionListeners = new ArrayList();
private ServletContext servletContext;
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
@Bean
public SessionRepositoryFilter extends ExpiringSession> springSessionRepositoryFilter(
SessionRepository sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
sessionRepositoryFilter.setHttpSessionStrategy(
(MultiHttpSessionStrategy) this.httpSessionStrategy);
}
else {
sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
}
return sessionRepositoryFilter;
}
@Autowired(required = false)
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer);
}
@Autowired(required = false)
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
this.httpSessionStrategy = httpSessionStrategy;
}
@Autowired(required = false)
public void setHttpSessionListeners(List listeners) {
this.httpSessionListeners = listeners;
}
}
Redis的完整源码
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements ImportAware {
private Integer maxInactiveIntervalInSeconds = 1800;
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
private String redisNamespace = "";
private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
private RedisSerializer
HttpSession是一个规范,可以替换他的默认的实现,包装HttpServletRequest,让他请求原始的getSession的时候覆盖掉默认的实现,比如我们可以使用Redis仓库去实现这个机制,HttpServletRequestWrapper我们只需要继承在这个包装类就可以覆盖掉自己需要覆盖的部分,那么获取session的时候默认的使用我们包装的HttpServletRequest,不再是请求原始的那个了,过滤链传递完成后所有的请求都使用这个引用啦!然后在使用我们的redis参考处理存储,删除等操作就好了。
包装HttpServletRequest
/**
* A {@link javax.servlet.http.HttpServletRequest} that retrieves the
* {@link javax.servlet.http.HttpSession} using a
* {@link org.springframework.session.SessionRepository}.
*
* @author Rob Winch
* @since 1.0
*/
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
private 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;
}
/**
* 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);//将当前的session保存在一个属性中!
}
}
@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 attrs = new HashMap();
Enumeration 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 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;
}
//通过sessionId 从仓库中获取session,并更新访问的时间
private S getSession(String sessionId) {
S session = SessionRepositoryFilter.this.sessionRepository
.getSession(sessionId);
if (session == null) {
return null;
}
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
//覆盖掉父类的方法
@Override
public HttpSessionWrapper getSession(boolean create) {
//包装Session添加其他的功能,看看当前是否有session
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//通过当前的Request 中cookie中获取seesionid 获取session
String requestedSessionId = getRequestedSessionId();
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
S session = getSession(requestedSessionId);
if (session != null) {
//当前的Session是有效的,然后在进行包装一下,防止当前没有经过包装
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.");
}
//当前这个是非法的session
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
//不创建新的Session
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();//从窗仓库中创建一个Session
session.setLastAccessedTime(System.currentTimeMillis());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
@Override
public ServletContext getServletContext() {
if (this.servletContext != null) {
return this.servletContext;
}
// Servlet 3.0+
return super.getServletContext();
}
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
@Override
public String getRequestedSessionId() {
return SessionRepositoryFilter.this.httpSessionStrategy
.getRequestedSessionId(this);
}
/**
* Allows creating an HttpSession from a Session instance.
*
* @author Rob Winch
* @since 1.0
*/
//这里对于session进行包装,添加ServletContext的引用,ServletContext需要web的环境。
private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {
HttpSessionWrapper(S session, ServletContext servletContext) {
super(session, servletContext);
}
@Override
public void invalidate() {
//删除掉Session
super.invalidate();
SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
setCurrentSession(null);
SessionRepositoryFilter.this.sessionRepository.delete(getId());
}
}
}
包装HttpServletResponseWrapper->OnCommittedResponseWrapper(包装当提交数据的时候做一些监听的操作)->SessionRepositoryResponseWrapper这个包装的意思就是返回值的时候做一些操作哦!
private final class SessionRepositoryResponseWrapper
extends OnCommittedResponseWrapper {
private final SessionRepositoryRequestWrapper request;
/**
* Create a new {@link SessionRepositoryResponseWrapper}.
* @param request the request to be wrapped
* @param response the response to be wrapped
*/
SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
HttpServletResponse response) {
super(response);
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
}
this.request = request;
}
//这个就是上层包装处理,触发提交session的操作,第一个就是保存数据库第二个就是返回Cookie
@Override
protected void onResponseCommitted() {
this.request.commitSession();
}
}
下面就是主要的流程啦!包装,然后传递给下面的过滤链,使用的就是我们包装过的Session,这里不是原始的Filter的过滤链,是实现了只过滤一次的过滤链,所以调用的方法不一样
@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();
}
}
/**
* Allows for easily ensuring that a request is only invoked once per request. This is a
* simplified version of spring-web's OncePerRequestFilter and copied to reduce the foot
* print required to use the session support.
*
* @author Rob Winch
* @since 1.0
*/
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.
* @param request the request
* @param response the response
* @param filterChain the filter chain
* @throws ServletException if request is not HTTP request
* @throws IOException in case of I/O operation exception
*/
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;
boolean hasAlreadyFilteredAttribute = request
.getAttribute(this.alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else {
// Do invoke this 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.
*
* Provides HttpServletRequest and HttpServletResponse arguments instead of the
* default ServletRequest and ServletResponse ones.
*
* @param request the request
* @param response the response
* @param filterChain the FilterChain
* @throws ServletException thrown when a non-I/O exception has occurred
* @throws IOException thrown when an I/O exception of some sort has occurred
* @see Filter#doFilter
*/
protected abstract void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException;
public void init(FilterConfig config) {
}
public void destroy() {
}
}
看这个Filter的头写的很详细,HttpSession被org.springframework.session.Session支持,这样的支持是通过包装类HttpSessionWrapper ->ExpiringSessionHttpSession->HttpSession
保存被Session仓库 SessionRepository 所支持。The SessionRepositoryFilter must be placed before any Filter that access the
/**
* Switches the {@link javax.servlet.http.HttpSession} implementation to be backed by a
* {@link org.springframework.session.Session}.
*
* The {@link SessionRepositoryFilter} wraps the
* {@link javax.servlet.http.HttpServletRequest} and overrides the methods to get an
* {@link javax.servlet.http.HttpSession} to be backed by a
* {@link org.springframework.session.Session} returned by the
* {@link org.springframework.session.SessionRepository}.
*
* The {@link SessionRepositoryFilter} uses a {@link HttpSessionStrategy} (default
* {@link CookieHttpSessionStrategy} to bridge logic between an
* {@link javax.servlet.http.HttpSession} and the
* {@link org.springframework.session.Session} abstraction. Specifically:
*
*
* - The session id is looked up using
* {@link HttpSessionStrategy#getRequestedSessionId(javax.servlet.http.HttpServletRequest)}
* . The default is to look in a cookie named SESSION.
* - The session id of newly created {@link org.springframework.session.ExpiringSession}
* is sent to the client using
*
- The client is notified that the session id is no longer valid with
* {@link HttpSessionStrategy#onInvalidateSession(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
*
*
*
*
* The SessionRepositoryFilter must be placed before any Filter that access the
* HttpSession or that might commit the response to ensure the session is overridden and
* persisted properly.
*
*
* @param the {@link ExpiringSession} type.
* @since 1.0
* @author Rob Winch
*/