Spring 之RedisSession详解
   
    
    
    基本使用
   
springRedisSession常用于解决多机部署的session统一问题,将session存入redis。以实现session的多机共识性。
Springboot redisSession需要加上
		<dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
    然后在启动类上加入
    
     @EnableRedisHttpSession
    
@SpringBootApplication
@EnableRedisHttpSession
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(PaymentApplication.class);
        springApplication.run(args);
    }
}
    
    
    Spring HttpRedis的原理
   
    SpringHttp实际上就是做了ServletFilter,Filter的类名为
    
     org.springframework.session.web.http.SessionRepositoryFilter
    
    
    
    Spring怎么寻Filter类的
   
我们通常Filter要实现Filter类,然后注入生成bean注入到Spring中,Spring 通过
    
    
    Sevlet的Filter
   
Tomcat是通过
org.apache.coyote.http11.Http11Processor#service
这个方法来创建request和response类的.
Springboot web初始化有4种filter
- CharacterEncodingFilter 主要对Request和response编码进行设置
	@Bean
	@ConditionalOnMissingBean
	public CharacterEncodingFilter characterEncodingFilter() {
		CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
		filter.setEncoding(this.properties.getCharset().name());
		filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
		filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
		return filter;
	}
		@Override
	protected void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		String encoding = getEncoding();
		if (encoding != null) {
			if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
				request.setCharacterEncoding(encoding);
			}
			if (isForceResponseEncoding()) {
				response.setCharacterEncoding(encoding);
			}
		}
		filterChain.doFilter(request, response);
	}
- formContentFilter
主要是将输入流转化为Map
	@Bean
	@ConditionalOnMissingBean(FormContentFilter.class)
	@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
	public OrderedFormContentFilter formContentFilter() {
		return new OrderedFormContentFilter();
	}
@Nullable
	private MultiValueMap<String, String> parseIfNecessary(HttpServletRequest request) throws IOException {
		if (!shouldParse(request)) {
			return null;
		}
		HttpInputMessage inputMessage = new ServletServerHttpRequest(request) {
			@Override
			public InputStream getBody() throws IOException {
				return request.getInputStream();
			}
		};
		return this.formConverter.read(null, inputMessage);
	}
- RequestContextFilter
		@Bean
		@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })
		@ConditionalOnMissingFilterBean(RequestContextFilter.class)
		public static RequestContextFilter requestContextFilter() {
			return new OrderedRequestContextFilter();
		}
@Override
	protected void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
		initContextHolders(request, attributes);
		try {
			filterChain.doFilter(request, response);
		}
		finally {
			resetContextHolders();
			if (logger.isTraceEnabled()) {
				logger.trace("Cleared thread-bound request context: " + request);
			}
			attributes.requestCompleted();
		}
	}
这个filter实际上就是为了设置两个ThreadLoacal.
	private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
		LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
		RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
		if (logger.isTraceEnabled()) {
			logger.trace("Bound request context to thread: " + request);
		}
	}
	
public abstract class RequestContextHolder  {
	private static final boolean jsfPresent =
			ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");
	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");
			.....
}
- WsFilter
这个filter 主要是对WebSocket协议的处理
@Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        // This filter only needs to handle WebSocket upgrade requests
        if (!sc.areEndpointsRegistered() ||
                !UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        // HTTP request with an upgrade header for WebSocket present
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        // Check to see if this WebSocket implementation has a matching mapping
        String path;
        String pathInfo = req.getPathInfo();
        if (pathInfo == null) {
            path = req.getServletPath();
        } else {
            path = req.getServletPath() + pathInfo;
        }
        WsMappingResult mappingResult = sc.findMapping(path);
        if (mappingResult == null) {
            // No endpoint registered for the requested path. Let the
            // application handle it (it might redirect or forward for example)
            chain.doFilter(request, response);
            return;
        }
        UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(),
                mapp=ingResult.getPathParams());
    }
    
    
    Filter是如何被设置进去的?
   
    在方法
    
     org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#selfInitialize
    
    进行Filter的注入
   
@SafeVarargs
	public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
			Class<? extends ServletContextInitializer>... initializerTypes) {
		this.initializers = new LinkedMultiValueMap<>();
		this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
				: Collections.singletonList(ServletContextInitializer.class);
		addServletContextInitializerBeans(beanFactory);
		addAdaptableBeans(beanFactory);
		List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
				.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
				.collect(Collectors.toList());
		this.sortedList = Collections.unmodifiableList(sortedInitializers);
		logMappings(this.initializers);
	}
protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
		MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory);
		addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter(multipartConfig));
		addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
		for (Class<?> listenerType : ServletListenerRegistrationBean.getSupportedTypes()) {
			addAsRegistrationBean(beanFactory, EventListener.class, (Class<EventListener>) listenerType,
					new ServletListenerRegistrationBeanAdapter());
		}
	}
    他会扫描到
    
     Filter
    
    ,
    
     Servlet
    
    ,
    
     ServletContextInitializer
    
    , 然后初始化,
   
private void selfInitialize(ServletContext servletContext) throws ServletException {
		prepareWebApplicationContext(servletContext);
		registerApplicationScope(servletContext);
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}
	}
最终被加入到Context:
 public void addMappingForUrlPatterns(
            EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
            String... urlPatterns) {
        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(filterDef.getFilterName());
        if (dispatcherTypes != null) {
            for (DispatcherType dispatcherType : dispatcherTypes) {
                filterMap.setDispatcher(dispatcherType.name());
            }
        }
        if (urlPatterns != null) {
            // % decoded (if necessary) using UTF-8
            for (String urlPattern : urlPatterns) {
                filterMap.addURLPattern(urlPattern);
            }
            if (isMatchAfter) {
                context.addFilterMap(filterMap);
            } else {
                context.addFilterMapBefore(filterMap);
            }
        }
        // else error?
    }
    
    
    SessionRepositoryFilter
   
sessionRepositoryFilter主要是实现了对Request和Response的包装
@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);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
				response);
		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
			wrappedRequest.commitSession();
		}
	}
如上图,将request和response包装成SessionRepositoryRequestWrapper,和 SessionRepositoryResponseWrapper
    
    
    Session原本是怎么存储的
   
上面讲了我们在MVC框架中拿到的request和response类实际为SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper类。
    在
    
     org.springframework.web.servlet.DispatcherServlet#doService
    
    会尝试获取session,如果没有session,将不会创建。如果在请求中创建session,将会返回sessionID.
   
    Session通过
    
     org.springframework.session.SessionRepository
    
    来存储,原生的实现类有
    
     org.springframework.session.MapSessionRepository
    
    ,原理就是一个map。
   
RedisSession的实现类有两个。
    
     org.springframework.session.data.redis.RedisSessionRepository
    
    和
   
    
     org.springframework.session.data.redis.RedisIndexedSessionRepository
    
    框架默认的是:
    
     org.springframework.session.data.redis.RedisIndexedSessionRepository
    
    
     两者有什么不同,怎么取舍:
    
   
RedisIndexedSessionRepository的功能比RedisSessionRepository复杂且多,主要表现在前者可以发出事件让监听
    
    
    Session的相关设置和操作
   
    
    
    创建sesion的方式
   
request.getSession() 就会创建session,因为
@Override
public HttpSessionWrapper getSession() {
		return getSession(true);
}
getsession没指定,那么就会默认为创建
那么操作就可以基本为
request.getSession().setAttribute("name","dong");
redis上的数据为:
     
   
也就是说:
    
     RedisIndexedSessionRepository
    
    ,会创建三个key。
   
    
    
    如何删除session
   
request.getSession().invalidate();
    
    
    如何设置Session的超时时间
   
    session的默认时间为:
    
     DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
    
    等于30分钟。
   
    在使用
    
     RedisIndexedSessionRepository
    
    的情况下,直接用注解 @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60)
   
    
    
    RedisSession中Redis的三个键值对存了些什么
   
就一个sesion而言,redis存了3个key
- 
     
 spring:session:sessions:b82b4153-ddb7-494c-b7fe-a28ae4f5db61
 
- 
     
 spring:session:sessions:expires:b2bfe5a3-698b-4a8d-a93f-4c0e584b92d6
 
- 
     
 spring:session:expirations:1656052140000
 
    设置的方法为
    
     org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#saveDelta
    
第一个key,主要存session中的一些键值对,以及创建时间,上一次请求时间,这个key的存活时间为5分钟+最大超时时间
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yzVm9fEx-1656056826363)(C:\Users\jm011254\AppData\Roaming\Typora\typora-user-images\image-20220624142828566.png)]
     
   
第二个key,主要是存真正的超时时间,设置的60秒 就会只有60秒
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
存这个的目的是为了更好查询到第三个key。
第三个key, 主要是设置key的超时时间,超时时间会在设置的基础上加5分钟‘
long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    存这个的目的是为了定时由
    
     org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions
    
    清理.(需要开启定时任务)。
   
    
    
    更改Redis的序列化方式
   
只需要指定redis的序列化就行了
@Bean(name = "springSessionDefaultRedisSerializer")
 public RedisSerializer getRedisSerializer() {
        return new FastJsonRedisSerializer(Object.class);
 }
    
    
    自定义NameSpace
   
只需要注解的时候,修改成为
    
     @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60,redisNamespace = "session")
    
    
    
    修改sessionRepository
   
通过创建SessionRepositoryCustomizer 来自定义sessionRepository
@Bean
    public SessionRepositoryCustomizer getsessionRepositoryCustomizer(){
        return new SessionRepositoryCustomizer() {
            @Override
            public void customize(SessionRepository sessionRepository) {
                RedisIndexedSessionRepository sessionRepository1 = (RedisIndexedSessionRepository) sessionRepository;
                sessionRepository1.setFlushMode(FlushMode.IMMEDIATE);
                sessionRepository1.setDatabase(2);
            }
        };
    }
    
    
    自定义SessionId解析器
   
    我们需要从request中获取sessionId吗,默认解析类是从cookie中解析的
    
     org.springframework.session.web.http.CookieHttpSessionIdResolver
    
    .
   
    如果我们需要定义从
    
     header
    
    中返回.
   
   @Bean
    public HttpSessionIdResolver getHttpSessionIdResolver(){
        return new HeaderHttpSessionIdResolver("token");
    }
加上上面这个就行了。
 
