Spring中自定义Session管理,Spring Session源码解析

Posted 香菜+

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring中自定义Session管理,Spring Session源码解析相关的知识,希望对你有一定的参考价值。

系列文章:Spring Boot学习大纲,可以留言自己想了解的技术点

目录

系列文章:Spring Boot学习大纲,可以留言自己想了解的技术点

1、session是什么?

1>session在哪里?

2>服务器怎么知道每次说话的是哪个session

3>session的使用

2、session的数据结构

3、tomcat中的session

3.1 session的创建时机和同步

3.2 session调用堆栈

3.3 源码分析

4、自定义一个Session的存储和构造,替换掉tomcat的session机制

4.1 思路

4.2 自定义session创建和管理

4.2.1>创建一个springboot项目

4.2.2> 创建一个自定义的session

4.2.3>创建一个session的容器,这里可以换成你想要的

4.2.4 替换servlet的实现

4.2.5 创建filter

4.2.6 测试controller

4.2.7 测试一下

5、Spring-session配置

5.1 创建springboot项目,

5.2 开启配置@EnableRedisHttpSession

5.3 配置文件

5.4 测试

5.5 确认数据存储到redis

6、Spring Session 源码分析

6.1 SessionRepositoryFilter

6.2 SessionRepository

7、总结


1、session是什么?

session用中文翻译就是会话,主要是为了维护web访问时的上下文信息保存,避免出现说完话就忘了对方是谁的情况

1>session在哪里?

session存储在服务器的内容中

2>服务器怎么知道每次说话的是哪个session

http访问的时候,header中有一个属性是sessionId,服务器根据session查找当前存在的属性

以chrome浏览器为例,访问一个基于tomcat服务器的网站的时候,

浏览器第一次访问服务器,服务器会在响应头添加Set-Cookie:“JSESSIONID=XXXXXXX”信息,要求客户端设置cooki

3>session的使用

//    获取session对象
HttpSession session = request.getSession();
//    获取
Object key = session.getAttribute("key");
session.setAttribute("key","value");

2、session的数据结构

session的数据结构没有固定的,怎么实现是看实现方式,这里介绍下常规认识的tomcat中的session数据结构

在tomcat中,session的数据结构是ConcurrentMap,key 是属性名,value是属性值

参考:org.apache.catalina.session.StandardSession

protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap();

后面我们要自定义一个session的存储方式。

3、tomcat中的session

3.1 session的创建时机和同步

客户端第一次请求request.getsession()时,也就是说客户端的请求中服务端第一次调用request.getsession()时,服务器会创建了session对象并保存在servlet容器的session集合中,同时生成一个sessionId,

向客户端发送要求设置cookie的响应(cookie中设置session id信息),客户端收到响应后,在客户端设置了一个jsessionid=xxxxxxx的cookie信息;

接下来客户端每次向服务器发送请求时,请求头都会带上该cookie信息(包含session id),那么之后的每次请求都能从servlet容器的session集合中找到客户端对应的session了,这样也就相当于保持了用户与服务器的交互状态。 

3.2 session调用堆栈

调试代码:

@GetMapping("/test/id")
public String test(@PathVariable(value = "id") String id, HttpServletRequest request) 
    HttpSession session = request.getSession();
    Long currentTime = timeService.getCurrentTime(id);
    System.out.println("参数 : "+ id +"   result  "+ currentTime);
    return "Hello";

看下断点的位置

3.3 源码分析

直接跟代码

HttpSession session = request.getSession();

下面流程画下来了,建议有条件的话跟一下代码,大概了解下

4、自定义一个Session的存储和构造,替换掉tomcat的session机制

4.1 思路

先看下tomcat整个数据的流程

思路1:直接替换掉tomcat的contextManger ,复杂了点,而且不适合Springboot,因为tomcat是内嵌的,如果换了web容器不一定适合

思路2:直接替换掉servlet中的session实现,在刚进入web容器的时候直接替换掉session实现,也就是在filter处

4.2 自定义session创建和管理

这里采用方案2,创建一个 filter,然后在入口处直接替换掉 request 中session的实现,直接上代码吧

4.2.1>创建一个springboot项目

不多说,一路next

4.2.2> 创建一个自定义的session

package com.xin.sessiontest.customize;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import java.util.*;

public class CustomizeSession implements HttpSession 
    private final Map<String, Object> attributeMap = new LinkedHashMap<>();
    private boolean isNew;
    private String id;
    public CustomizeSession(String id) 
        if (id == null || "".equals(id.trim())) 
            id = UUID.randomUUID().toString().replace("-", "");
        
        this.id = id;
        this.isNew = true;
        System.out.println("新session id : " + this.id);
    
    public long getCreationTime() 
        return 0;
    
    public String getId() 
        return this.id;
    
    public long getLastAccessedTime() 
        return 0;
    
    public ServletContext getServletContext() 
        return null;
    
    public void setMaxInactiveInterval(int interval) 
    
    public int getMaxInactiveInterval() 
        return 0;
    
    public HttpSessionContext getSessionContext() 
        return null;
    
    public Object getAttribute(String name) 
        return this.attributeMap.get(name);
    
    public Object getValue(String name) 
        return null;
    
    public Enumeration getAttributeNames() 
        System.out.println("CustomizeSession的getAttributeNames方法");
        return Collections.enumeration(this.attributeMap.keySet());
    
    public String[] getValueNames() 
        return new String[0];
    
    public void setAttribute(String name, Object value) 
        this.attributeMap.put(name, value);
    
    public void putValue(String name, Object value) 
    
    public void removeAttribute(String name) 
    
    public void removeValue(String name) 
    
    public void invalidate() 
    
    public boolean isNew() 
        return this.isNew;
    
    public void setIsNew(boolean isNew) 
        this.isNew = isNew;
    

4.2.3>创建一个session的容器,这里可以换成你想要的

key 是sessionId,value是session

package com.xin.sessiontest.customize;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CustomizeSessionContainer 
    public static final String DEFAULT_SESSION_ID_NAME = "JSESSIONID";
    private static final Map<String, CustomizeSession> sessionMap = new ConcurrentHashMap<>();

    public static CustomizeSession getSession(String sessionId, HttpServletResponse response) 
        return getSession(sessionId, true, response);
    
    public static CustomizeSession getSession(String sessionId, boolean create, HttpServletResponse response) 
        if (sessionId == null) 
            sessionId = "";
        
        CustomizeSession session = sessionMap.get(sessionId);
        if (session != null) 
            session.setIsNew(false);
            return session;
        
        if (create) 
            session = new CustomizeSession(sessionId);
            sessionMap.put(session.getId(), session);
            response.addCookie(new Cookie(CustomizeSessionContainer.DEFAULT_SESSION_ID_NAME,
                    session.getId()));
        
        return session;
    

4.2.4 替换servlet的实现

package com.xin.sessiontest.customize;
import javax.servlet.http.*;
public class CustomizeHttpServletRequest extends HttpServletRequestWrapper 
    private HttpServletResponse response;
    public CustomizeHttpServletRequest(HttpServletRequest request, HttpServletResponse response) 
        super(request);
        this.response = response;
    
    @Override
    public HttpSession getSession() 
        return this.getSession(true);
    

    @Override
    public HttpSession getSession(boolean create) 
        Cookie[] cookies = this.getCookies();
        String sessionId = "";
        if (cookies != null) 
            for (Cookie cookie : cookies) 
                if (CustomizeSessionContainer.DEFAULT_SESSION_ID_NAME.equals(cookie.getName())) 
                    sessionId = cookie.getValue();
                    break;
                
            
        

        HttpSession customizeSession = CustomizeSessionContainer.getSession(sessionId, create, response);
        return customizeSession;
    

4.2.5 创建filter

package com.xin.sessiontest.filter;

import com.xin.sessiontest.customize.CustomizeHttpServletRequest;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = "/*", filterName = "FirstFilter")
public class CustomizeSessionFilter implements Filter 
    public void init(FilterConfig filterConfig) throws ServletException 
        System.out.println("CustomizeSessionFilter init");
    
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException 
        CustomizeHttpServletRequest request = new CustomizeHttpServletRequest((HttpServletRequest) req, (HttpServletResponse) resp);
        chain.doFilter(request, resp);
    
    public void destroy() 
        System.out.println("CustomizeSessionFilter destroy");
    

4.2.6 测试controller

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
public class TestController 
    @GetMapping("/")
    public String test(HttpServletRequest request)
        HttpSession session = request.getSession();
        session.setAttribute("key","value");
        return "hello";
    

4.2.7 测试一下

http://localhost:8080/

看到后台是有打印session创建,证明已经接管了

5、Spring-session配置

Spring Session 它提供一组 API 和实现, 用于管理用户的 session 信息, 专注于解决 session 管理问题,轻易把session存储到第三方存储容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多种存储session的容器的方式。主要解决分布式session共享问题

5.1 创建springboot项目,

勾选Spring Session 和Spring Data Redis

或者直接贴进去依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

5.2 开启配置@EnableRedisHttpSession

@SpringBootApplication
@EnableRedisHttpSession
public class SessionTestApplication 

    public static void main(String[] args) 
        SpringApplication.run(SessionTestApplication.class, args);
    

5.3 配置文件

spring:
  # 选择redis为session存储
  session:
    store-type: redis
  # 配置redis
  redis:
    host: 172.26.1.152
    port: 6379
    database: 9

# springsSession过期时间
server:
  servlet:
    session:
      timeout: 30m

5.4 测试

http://localhost:8080/

@RestController
public class TestController 
    @GetMapping("/")
    public String test(HttpServletRequest request)
        HttpSession session = request.getSession();
        session.setAttribute("caraway","香菜");
        return "hello";
    

5.5 确认数据存储到redis

配置到9号DB,这里可以看到有一个key :caraway

6、Spring Session 源码分析

@EnableRedisHttpSession 导入了一个RedisHttpSessionConfiguration.class配置,

这个配置首先首先添加了一个组件RedisOperationsSessionRepository,redis操作session的dao其内部所有的方法都是增删改查的

看下百度到的图,貌似和我们实现的很像

6.1 SessionRepositoryFilter

用来切换HttpSession至Spring Session,包装HttpServletRequest和HttpServletResponse

@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter 
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException 
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
        SessionRepositoryFilter<S>.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);
        try 
            filterChain.doFilter(wrappedRequest, wrappedResponse);
         finally 
            wrappedRequest.commitSession();
        

看下重点:

1、order注解,是int的最小值,这个保证所有的请求第一个经过这个filter

2、doFilterInternal 包装request 和response

6.2 SessionRepository

看下实现类,看起来实现没啥,创建一个session,保存session数据,查找session,都是常规操作

public class RedisSessionRepository implements SessionRepository<RedisSession> 
    public RedisSession createSession() 
        MapSession cached = new MapSession();
        cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
        RedisSession session = new RedisSession(cached, true);
        session.flushIfRequired();
        return session;
    
    public void save(RedisSession session) 
        if (!session.isNew) 
            String key = this.getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
            Boolean sessionExists = this.sessionRedisOperations.hasKey(key);
            if (sessionExists == null || !sessionExists) 
                throw new IllegalStateException("Session was invalidated");
            
        
        session.save();
    
        public RedisSession findById(String sessionId) 
            String key = this.getSessionKey(sessionId);
            Map<String, Object> entries = this.sessionRedisOperations.opsForHash().entries(key);
            if (entries.isEmpty()) 
                return null;
             else 
                MapSession session = (new RedisSessionMapper(sessionId)).apply(entries);
                if (session.isExpired()) 
                    this.deleteById(sessionId);
                    return null;
                 else 
                    return new RedisSession(session, false);
                
            
    

7、总结

全篇写了很多,简单来说就是session的管理,从web项目中接管session。

自定义session管理和Spring session 都是相同的原理

嵌入Filter,替换Request,自定义Session容器

源码下载地址:https://download.csdn.net/download/perfect2011/87472028

最后的最后求一个免费的赞同,爱心发电 

以上是关于Spring中自定义Session管理,Spring Session源码解析的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Security 中自定义权限表达式

Spring Boot + Swagger + 自定义 swagger-ui.html

spring Security项目快速搭建

如何在 Spring Boot 1.4 中自定义 Jackson

学习笔记Spring中自定义注解

Spring Boot中自定义注解