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");
}
加上上面这个就行了。