zoukankan      html  css  js  c++  java
  • Spring-Session实现Session共享实现原理以及源码解析

    知其然,还要知其所以然 !

    本篇介绍Spring-Session的整个实现的原理。以及对核心的源码进行简单的介绍!

    实现原理介绍

    实现原理这里简单说明描述:

    就是当Web服务器接收到http请求后,当请求进入对应的Filter进行过滤,将原本需要由web服务器创建会话的过程转交给Spring-Session进行创建,本来创建的会话保存在Web服务器内存中,通过Spring-Session创建的会话信息可以保存第三方的服务中,如:redis,mysql等。Web服务器之间通过连接第三方服务来共享数据,实现Session共享!

    实现原理结构草图

    整个实现流程和源码详细介绍

    本次源码介绍基于上一篇内容,并且在保存Session的时候只会分析使用JedisConnectionFactory实现的RedisConnectionFactory !

    1.SessionRepositoryFilter和JedisConnectionFactory注册过程

    流程:

    SessionRepositoryFilter和JedisConnectionFactory注册过程

    说明:

    1.、启动WEB项目的时候,会读取web.xml,读取顺序content-param --> listener --> filter --> servlet
    
    2.、ContextLoaderListener监听器的作用就是启动Web容器时,自动装配ApplicationContext的配置信息
    
    3、初始化根web应用程序上下文。
    
    4、SpringHttpSessionConfiguration注册 springSessionRepositoryFilter :bean,RedisHttpSessionConfiguration 注册 sessionRedisTemplate : bean  和 sessionRepository : bean
    
    45、配置文件配置JedisConnectionFactory implements RedisConnectionFactory ,创建 jedisConnectionFactory bean
    
    

    代码分析如下:

    1. web.xml ,加载了xml配置文件,并初始化web应用上下文
      <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring/*xml</param-value>
      </context-param>
    
    
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
      
    

    2.application-session.xml中,配置 RedisHttpSessionConfiguration的bean和JedisConnectionFactory的bean,web应用初始化加载bean!

    <!--创建一个Spring Bean的名称springSessionRepositoryFilter实现过滤器。
        筛选器负责将HttpSession实现替换为Spring会话支持。在这个实例中,Spring会话得到了Redis的支持。-->
        <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
    
        <!--创建了一个RedisConnectionFactory,它将Spring会话连接到Redis服务器。我们配置连接到默认端口(6379)上的本地主机!-->
        <!--集群Redis-->
        <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
            <!--Redis-CLuster-->
            <constructor-arg index="0" ref="redisClusterConfig"/>
    
            <!--配置Redis连接池 ,可以不配置,使用默认就行!-->
            <constructor-arg index="1" ref="jedisPoolConfig"/>
        </bean>
        
    
        /**
    	 * 初始化根web应用程序上下文。
    	 */
    	@Override
    	public void contextInitialized(ServletContextEvent event) {
    		initWebApplicationContext(event.getServletContext());
    	}
    	
    

    4.RedisHttpSessionConfiguration类图

    RedisHttpSessionConfiguration类图

    RedisHttpSessionConfiguration注释
    RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration

    public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
    		implements EmbeddedValueResolverAware, ImportAware {
    		
    

    4.1 SpringHttpSessionConfiguration 创建一个名称为springSessionRepositoryFilter的bean

    @Bean
    	public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
    			SessionRepository<S> sessionRepository) {
    		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(
    				sessionRepository);
    		sessionRepositoryFilter.setServletContext(this.servletContext);
    		if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
    			sessionRepositoryFilter.setHttpSessionStrategy(
    					(MultiHttpSessionStrategy) this.httpSessionStrategy);
    		}
    		else {
    			sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
    		}
    		return sessionRepositoryFilter;
    	}
    
    
    

    4.2 创建RedisHttpSessionConfiguration#RedisTemplate bean的名称为sessionRedisTemplate

    @Bean
    	public RedisTemplate<Object, Object> sessionRedisTemplate(
    			RedisConnectionFactory connectionFactory) {
    			//实例化 RedisTemplate 
    		RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
    		//设置key序列化 StringRedisSerializer
    		template.setKeySerializer(new StringRedisSerializer());
    		//设置Hash key  StringRedisSerializer
    		template.setHashKeySerializer(new StringRedisSerializer());
    		if (this.defaultRedisSerializer != null) {
    			template.setDefaultSerializer(this.defaultRedisSerializer);
    		}
    		//设置 connectionFactory。第五步创建的(实际connectionFactory加载过程和讲解过程顺序不一样)
    		template.setConnectionFactory(connectionFactory);
    		return template;
    	}
    
    

    4.3 创建RedisHttpSessionConfiguration#RedisOperationsSessionRepository bean的名称为sessionRepository

    	@Bean
    	public RedisOperationsSessionRepository sessionRepository(
    	//使用sessionRedisTemplate bean
    			@Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,
    			ApplicationEventPublisher applicationEventPublisher) {
    			
    			//實例化RedisOperationsSessionRepository
    		RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
    				sessionRedisTemplate);
    				//設置applicationEventPublisher
    		sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
    		//設置最大的失效時間 maxInactiveIntervalInSeconds = 1800
    		sessionRepository
    				.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
    		if (this.defaultRedisSerializer != null) {
    			sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
    		}
    
    		String redisNamespace = getRedisNamespace();
    		if (StringUtils.hasText(redisNamespace)) {
    			sessionRepository.setRedisKeyNamespace(redisNamespace);
    		}
    
    		sessionRepository.setRedisFlushMode(this.redisFlushMode);
    		return sessionRepository;
    	}
    	
    
    1. 创建 RedisConnectionFactory bean为 jedisConnectionFactory
      JedisConnectionFactory类图
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    
    

    2.SessionRepositoryFilter添加到FilterChain

    流程:

    SessionRepositoryFilter添加到FIlterChain

    说明:

    1 2、在Servlet3.0规范中,Servlet容器启动时会自动扫描javax.servlet.ServletContainerInitializer的实现类,在实现类中我们可以定制需要加载的类。 通过注解@HandlesTypes(WebApplicationInitializer.class),让Servlet容器在启动该类时,会自动寻找所有的WebApplicationInitializer实现类。
    
    2.1、insertSessionRepositoryFilter 方法通过filterName获取 SessionRepositoryFilter ,并创建了 new DelegatingFilterProxy(filterName);
    
    3 4、然后将filter添加到FilterChain中
    
    
    

    1.ServletContainerInitializer的实现类加载和通过注解@HandlesTypes(WebApplicationInitializer.class)实现类的加载

    //加载实现类
    @HandlesTypes(WebApplicationInitializer.class)
    //SpringServletContainerInitializer实现ServletContainerInitializer
    public class SpringServletContainerInitializer implements ServletContainerInitializer {
    
    //------------
    
    
    

    2.AbstractHttpSessionApplicationInitializer实现WebApplicationInitializer进行加载

    
    @Order(100)
    public abstract class AbstractHttpSessionApplicationInitializer
    		implements WebApplicationInitializer {
    
    

    2.1 onStartup

    public void onStartup(ServletContext servletContext) throws ServletException {
    		beforeSessionRepositoryFilter(servletContext);
    		if (this.configurationClasses != null) {
    			AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
    			rootAppContext.register(this.configurationClasses);
    			servletContext.addListener(new ContextLoaderListener(rootAppContext));
    		}
    		//添加Filter
    		insertSessionRepositoryFilter(servletContext);
    		afterSessionRepositoryFilter(servletContext);
    	}
    
    

    2.1.1.insertSessionRepositoryFilter

    
        /**
    	 * 注册springSessionRepositoryFilter
    	 * @param servletContext the {@link ServletContext}
    	 */
    	private void insertSessionRepositoryFilter(ServletContext servletContext) {
    // DEFAULT_FILTER_NAME = "springSessionRepositoryFilter"
    		String filterName = DEFAULT_FILTER_NAME;
    //通过filterName创建 DelegatingFilterProxy
    		DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(
    				filterName);
    		String contextAttribute = getWebApplicationContextAttribute();
    		if (contextAttribute != null) {
    			springSessionRepositoryFilter.setContextAttribute(contextAttribute);
    		}
    //根据filterName和上下文添加Filter到FilterChain
    		registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
    	}
    
    
    1. registerFilter
    	private void registerFilter(ServletContext servletContext,
    			boolean insertBeforeOtherFilters, String filterName, Filter filter) {
    		Dynamic registration = servletContext.addFilter(filterName, filter);
    		if (registration == null) {
    			throw new IllegalStateException(
    					"Duplicate Filter registration for '" + filterName
    							+ "'. Check to ensure the Filter is only configured once.");
    		}
    		//是否支持异步,默认 true
    		registration.setAsyncSupported(isAsyncSessionSupported());
    		//得到DispatcherType springSessionRepositoryFilter
    		EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes();
    		//添加一个带有给定url模式的筛选器映射和由这个FilterRegistration表示的过滤器的分派器类型。 过滤器映射按照添加它们的顺序进行匹配。
    		registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters,
    				"/*");
    	}
    
    1. addFilter将Filter添加到ServletContext中
        public FilterRegistration.Dynamic addFilter(
            String filterName, Filter filter);
    
    

    3.SessionRepositoryFilter拦截过程

    流程:

    SessionRepositoryFilter拦截过程

    说明:

    1、请求被DelegatingFilterProxy : 拦截到,然后执行doFilter方法,在doFilter中找到执行的代理类。
    2、OncePerRequestFilter : 代理Filter执行doFilter方法,然后调用抽象方法doFilterInternal
    3、SessionRepositoryFilter 继承了OncePerRequestFilter,实现了doFilterInternal,这个方法一个封装一个wrappedRequest,通过执行commitSession保存session信息到redis
    
    

    1请求进来,被DelegatingFilterProxy 拦截到,在web.xml中进行了配置
    1.1 执行doFilter

    如果没有指定目标bean名称,请使用筛选器名称。
    @Override
    	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
    			throws ServletException, IOException {
    
    		// 如果需要,延迟初始化委托。 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 or DispatcherServlet registered?");
    					}
    					this.delegate = initDelegate(wac);
    				}
    				delegateToUse = this.delegate;
    			}
    		}
    
    		// 让委托执行实际的doFilter操作
    		invokeDelegate(delegateToUse, request, response, filterChain);
    	}
    

    1.2 initDelegate

    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    //可以获取到SessionRepositoryFilter [备注1]
    		Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
    		if (isTargetFilterLifecycle()) {
    			delegate.init(getFilterConfig());
    		}
    		return delegate;
    	}
    
    //[备注1] 因为 :SessionRepositoryFilter是一个优先级最高的javax.servlet.Filter
    /*
    @Order(SessionRepositoryFilter.DEFAULT_ORDER)
    public class SessionRepositoryFilter<S extends ExpiringSession>
    		extends OncePerRequestFilter {
    
    */
    
    1. delegate.doFilter();
    	protected void invokeDelegate(
    			Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
    			throws ServletException, IOException {
            //代理去执行doFilter,代理为SessionRepositoryFilter
    		delegate.doFilter(request, response, filterChain);
    	}
    
    
    

    2.1 OncePerRequestFilter#doFilter

    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) {
    
    			//在不调用此过滤器的情况下进行…
    			filterChain.doFilter(request, response);
    		}
    		else {
    			// 调用这个过滤器…
    			request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);
    			try {
    			//doFilterInternal是个抽象方法
    				doFilterInternal(httpRequest, httpResponse, filterChain);
    			}
    			finally {
    				// 删除此请求的“已过滤”请求属性。
    				request.removeAttribute(this.alreadyFilteredAttributeName);
    			}
    		}
    	}
    
    
    1. 执行SessionRepositoryFilter#doFilterInternal
    @Override
    	protected void doFilterInternal(HttpServletRequest request,
    			HttpServletResponse response, FilterChain filterChain)
    			throws ServletException, IOException {
    		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
            //使用HttpServletRequest 、HttpServletResponse和servletContext创建一个SessionRepositoryRequestWrapper
    
    		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
    				request, response, this.servletContext);
    		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
    				wrappedRequest, response);
    
    //使用CookieHttpSessionStrategy重新包装了 HttpServletRequest
    		HttpServletRequest strategyRequest = this.httpSessionStrategy
    				.wrapRequest(wrappedRequest, wrappedResponse);
    		HttpServletResponse strategyResponse = this.httpSessionStrategy
    				.wrapResponse(wrappedRequest, wrappedResponse);
    
    		try {
    		//执行其他过滤器
    			filterChain.doFilter(strategyRequest, strategyResponse);
    		}
    		finally {
    		//保存session信息
    			wrappedRequest.commitSession();
    		}
    	}
    
    

    4 .wrappedRequest.commitSession() 看下第四大点分析

    4.SessionRepository保存session数据

    流程:
    SessionRepository保存session数据

    说明:

    1、提交session保存
    2、获取当前session,这一步比较重要,获取了一个HttpSessionWrapper,这个HttpSessionWrapper替换了HTTPSession
    3、wrappedSession获取当前的Session
    4、使用 RedisTemplate 保存Session内容,并通过调用RedisConnection 使用它的实现类JedisClusterConnection获取redis连接
    
    

    1.commitSession

    /**
    *使用HttpSessionStrategy将会话id写入响应。 *保存会话。
    */
    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);
    				}
    			}
    		}
    

    2.getCurrentSession

    会话存储库请求属性名。
    public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class
    			.getName();
    			
    private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR
    			+ ".CURRENT_SESSION";
    
    private HttpSessionWrapper getCurrentSession() {
    			return (HttpSessionWrapper)
    			//获取session
    			getAttribute(CURRENT_SESSION_ATTR);
    		}
    		
       /**
         * 此方法的默认行为是在包装请求对象上调用getAttribute(字符串名称)。
         */
        public Object getAttribute(String name) {
        //这里的request就是上面封装的
            return this.request.getAttribute(name);
        }
    

    3 .wrappedSession.getSession

    //返回 RedisSession
    S session = wrappedSession.getSession();
    //-------------------------
    public S getSession() {
    		return this.session;
    	}
    class ExpiringSessionHttpSession<S extends ExpiringSession> implements HttpSession {
    	private S session;
    
    
    final class RedisSession implements ExpiringSession {
    
    

    4.save,实际是调用 RedisOperationsSessionRepository的 RedisOperations 操作

    SessionRepositoryFilter.this.sessionRepository.save(session);
    
    //this.sessionRepository =  SessionRepository<S> sessionRepository;
    
    //--------------------------------
    //这个RedisOperationsSessionRepository是之前就创建好的
    public class RedisOperationsSessionRepository implements
    		FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,
    		MessageListener {
    		
    		
    public interface FindByIndexNameSessionRepository<S extends Session>
    		extends SessionRepository<S> {
    		
    //---------------------------
    
    
    	public void save(RedisSession session) {
    	    //4.1saveDelta
    		session.saveDelta();
    		if (session.isNew()) {
    		    //4.2调用
    			String sessionCreatedKey = getSessionCreatedChannel(session.getId());
    			//4.3convertAndSend
    			//RedisOperations = this.sessionRedisOperations
    			this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
    			session.setNew(false);
    		}
    	}
    	
    	
    

    其中RedisOperationsSessionRepository 里面介绍保存的详细过程,具体请看文档说明:

    Class RedisOperationsSessionRepository

    因为 RedisTemplate implements RedisOperations,实际进行操作的是RedisTemplate,RedisTemplate通过RedisConnection进行数据add和remove等

    public class RedisTemplate<K, V>
    extends RedisAccessor
    implements RedisOperations<K, V>, BeanClassLoaderAware
    

    总结

    本系列到这里也就结束了,本次话的整个流程图,会上传到github上,使用Jude打开就可以看!

    如果有什么地方写的不对或者有想和我一起探讨一下的,欢迎加我的QQ或者QQ群!

    记录一个小点:

    Spring Session + Redis实现分布式Session共享 有个非常大的缺陷, 无法实现跨域名共享session , 只能在单台服务器上共享session , 因为是依赖cookie做的 , cookie 无法跨域 pring Session一般是用于多台服务器负载均衡时共享Session的,都是同一个域名,不会跨域。你想要的跨域的登录,可能需要SSO单点登录。

    参考博文

    【Spring】Spring Session的简单搭建与源码阅读

    利用spring session解决共享Session问题

    Spring Session解决分布式Session问题的实现原理

    spring-session简介、使用及实现原理


    本系列教程

    【第一篇】Spring-Session实现Session共享入门教程

    【第二篇】Spring-Session实现Session共享Redis集群方式配置教程

    【第三篇】Spring-Session实现Session共享实现原理以及源码解析

    本系列的源码下载地址:learn-spring-session-core

    备注: 由于本人能力有限,文中若有错误之处,欢迎指正。


    谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!


    Java编程技术乐园:一个分享编程知识的公众号。跟着老司机一起学习干货技术知识,每天进步一点点,让小的积累,带来大的改变!

    扫描关注,后台回复【秘籍】,获取珍藏干货! 99.9%的伙伴都很喜欢

    image.png | center| 747x519

    © 每天都在变得更好的阿飞云
  • 相关阅读:
    log4j基本使用方法
    Spring MVC中页面向后台传值的几种方式
    JXL操作Excel
    模板
    url&视图
    Git for PyCharm
    ServletConfig和ServletContext
    Exception和IOException之间的使用区别
    java学习一目了然——异常必知
    java学习一目了然——IO
  • 原文地址:https://www.cnblogs.com/aflyun/p/8532230.html
Copyright © 2011-2022 走看看