最近发现,SpringSession的好像有点不对我的胃口,有些难控制,于是我就自己照着它写了自己的Session实现。
首先,我们需要复习 Java Web 的知识,想想我们是如何操作 Session 的: session = request.getSession(),很明显是通过HttpServletRequest获取的,因为每次用户请求过来,我们服务端都会获取到请求携带的唯一Cookie SessionId。根据这点来看,我们必然要通过 HttpServletRequest 获取到我们自己的 Session,这样我们对于 Session 的操作才是使用我们自定义的。
但是想想, HttpServletRequest是不可能返回我们自定的 HttpSession的,所以我们还要自定义一个HttpServletRequest的包装类,使得每次请求获取的都是我们自己的HttpSession。
但是,又有一个难点,如何确保HttpServletRequest是我们定义的呢?
这里就需要Filter了,我们还需要自定义一个 Filter,这个Filter不干其它的事情,就负责把HttpServletRquest 换成我们自定义的包装类。
整理一下,我们需要三件利器,帮我们完成自定义的 Session 操作,
1: 首当其冲的便是我们自己的HttpSession,它需要实现HttpSession这个接口,
我叫他 CustomRedisHttpSession
2: 我们需要自己的HttpServletRquest帮我们获取到自定义的 CustomRedisHttpSession
我叫他 CustomSessionHttpServletRequestWrapper, 继承了 HttpServletRequestWrapper
3: 还需要一个只负责将HttpServletRequest包装成 CustomSessionHttpServletRequestWrapper
的Fitler,我叫他 CustomSessionRequestFilter ,实现了 Filter 接口
好的,有了思路,就开始实现,但是有许多坑需要注意,HttpSession 是不能注入属性的,所以我们需要手动创建一个类,来获取 bean:
//此类作为 获取 bean 的工具类
@Component(value="applicationContextUtil")
public class ApplicationContextUtil implements ApplicationContextAware {
//IOC 上下文对象
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtil.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
assertApplicationContext();
return applicationContext;
}
/**
* 三种获取 Bean 的方法
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String beanName) {
assertApplicationContext();
return (T) applicationContext.getBean(beanName);
}
public static <T> T getBean(Class<T> clazz) {
assertApplicationContext();
return applicationContext.getBean(clazz);
}
public static <T> T getBean(String penName,Class<T> clazz){
assertApplicationContext();
return applicationContext.getBean(penName,clazz);
}
//判断上下文对象是否未注入
private static void assertApplicationContext() {
if (ApplicationContextUtil.applicationContext == null) {
throw new RuntimeException("没有注入 applicationContext");
}
}
}
既然是实现 Redis 的Session,那么我们就需要有 RedisTemplate:
@SpringBootConfiguration
public class RedisConfig{
/**
* 配置Session RedisTemplate,用于Session的操作
* @return RedisTemplate<String,Serializable>r
*/
//两个参数LettuceConnectionFactory,Jackson2JsonRedisSerializer 不了解的先学习关于
//Redis和SpringBoot的整合
@Bean(name="sessionRedisTemplate")
public RedisTemplate<String,Object> getSessionRedisTemplate(
@Qualifier(value="sessionLettuceConnectionFactory")
LettuceConnectionFactory sessionLettuceConnectionFactory,
@Qualifier(value="jackson2JsonRedisSerializer")
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer){
//构造 sessionRedisTemplate
RedisTemplate<String,Object> sessionRedisTemplate = new RedisTemplate<>();
sessionRedisTemplate.setConnectionFactory(sessionLettuceConnectionFactory);
/**
* 使用 String 作为 Key 的序列化器,使用 jackson2JsonRedisSerializer 作为 Value 的序列化器
*/
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
sessionRedisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
sessionRedisTemplate.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用 jackson2JsonRedisSerializer
sessionRedisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用 jackson2JsonRedisSerializer
sessionRedisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
sessionRedisTemplate.afterPropertiesSet();
return sessionRedisTemplate;
}
}
重头戏,先实现三大利器中的 Filter 吧,多一句嘴,在SpringBoot中使用Filter的方法需要大家自己有相关知识,把 Filter注册到 SpringBoot中 :
/**
* 此过滤器拦截所有请求,也放行所有请求,但是只要与Session操作的有关的请求都换被
* 替换成:CustomSessionHttpServletRequestWrapper包装请求,
* 这个请求会获取自定义的HttpSession
*/
public class CustomSessionRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain)
throws IOException, ServletException {
//将请求替换成自定义的 CustomSessionHttpServletRequestWrapper 包装请求
HttpServletRequest customSessionHttpServletRequestWrapper =
new CustomSessionHttpServletRequestWrapper
((HttpServletRequest)servletRequest,(HttpServletResponse)servletResponse);
//获取请求的路径
String sessionPath=
customSessionHttpServletRequestWrapper.getServletPath();
//如果是与Session操作有关的请求,就替换成自定请求包装类,SESSION_PATH是我程序中的
//一个常量,也就是你需要操作Session的那个RequestMapping路径
//并继续调用doFilter执行责任链
if(sessionPath.equals(SESSION_PATH)){
HttpServletResponse httpServletResponse =
(HttpServletResponse)servletResponse;
//这里的责任链就是使用了自定义的 HttpServletRquest执行下去,非常的巧妙。
filterChain.doFilter(customSessionHttpServletRequestWrapper,
httpServletResponse);
return;
}
//如果是普通请求就放行
filterChain.doFilter(servletRequest,servletResponse);
}
/**
* init 和 destroy 是管理 Filter的生命周期的,与逻辑无关,所以无需实现
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
然后自定义 HttpServletRequest :
public class CustomSessionHttpServletRequestWrapper extends HttpServletRequestWrapper{
private HttpServletRequest httpServletRequest;
private HttpServletResponse httpServletResponse;
//自定义Session
private CustomRedisHttpSession customRedisHttpSession;
public CustomSessionHttpServletRequestWrapper(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
super(httpServletRequest);
this.httpServletRequest = httpServletRequest;
this.httpServletResponse = httpServletResponse;
Cookie[] cookies = httpServletRequest.getCookies();
this.customRedisHttpSession = new
CustomRedisHttpSession(httpServletRequest,httpServletResponse,cookies);
}
//这个方法就是最重要的,通过它获取自定义的 HttpSession
@Override
public HttpSession getSession() {
return this.customRedisHttpSession;
}
}
核心:RedisHttpSession ,有些方法我没有实现,也就不在代码中贴了, 说一下,session在 Redis中我选择的 Hash 结构存储,以 sessionId 作为Key:
//首先我说过,HttpSession是不能注入属性的,所以就需要依赖 上面定义的那个 工具类,获取bean
//如果不加此注解,你的属性就会为空,获取不到
@DependsOn("applicationContextUtil")
@SpringBootConfiguration
public class CustomRedisHttpSession implements HttpSession {
private HttpServletRequest httpServletRequest;
private HttpServletResponse httpServletResponse;
private Cookie[] cookies;
//sessionId
private String sessionId;
public CustomRedisHttpSession(){}
public CustomRedisHttpSession(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,Cookie[] cookies){
this.httpServletRequest = httpServletRequest;
this.httpServletResponse = httpServletResponse;
this.cookies = cookies;
this.sessionId = getCookieSessionId(cookies);
}
/**
* 获取指定属性值
* @param key 属性key
* @return key对应的value
*/
@Override
public Object getAttribute(String key) {
if(sessionId != null){
return sessionRedisTemplate.opsForHash().get(sessionId,key);
}
return null;
}
/**
* 之前说过了,此类属性不能注入,只能通过手动获取
*/
@SuppressWarnings("unchecked")
private final RedisTemplate<String,Object> sessionRedisTemplate =
=ApplicationContextUtil.getBean("sessionRedisTemplate",RedisTemplate.class);
//sessionId 的前缀
private static final String SESSIONID_PRIFIX="yangxiaoguang";
/**
* 设置属性值
* @param key key
* @param value value
*/
@Override
public void setAttribute(String key, Object value) {
if (sessionId != null) {
sessionRedisTemplate.opsForHash().put(sessionId, key, value);
}else{
//如果是第一次登录,那么生成 sessionId,将属性值存入redis,设置过期时间,并设置浏览器cookie
this.sessionId = SESSIONID_PRIFIX + UUID.randomUUID();
setCookieSessionId(sessionId);
sessionRedisTemplate.opsForHash().put(sessionId, key, value);
sessionRedisTemplate.expire(sessionId, sessionTimeout, TimeUnit.SECONDS);
}
}
//session的过期时间,8小时
private final int sessionTimeout=28800;
//将sessionId存入浏览器
private void setCookieSessionId(String sessionId){
Cookie cookie = new Cookie(SESSIONID,sessionId);
cookie.setPath("/");
cookie.setMaxAge(sessionTimeout);
this.httpServletResponse.addCookie(cookie);
}
/**
* 移除指定的属性
* @param key 属性 key
*/
@Override
public void removeAttribute(String key) {
if(sessionId != null){
sessionRedisTemplate.opsForHash().delete(sessionId,key);
}
}
/**
* session失效,删除当前用户的所有信息
*/
@Override
public void invalidate() {
//删除在 redis 中的信息
if(sessionId != null){
sessionRedisTemplate.delete(sessionId);
sessionId = null;
if(cookies != null){
for(Cookie cookie : cookies){
if(SESSIONID.equals(cookie.getName())){
//使cookie无效
cookie.setMaxAge(0);
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
cookies = null;
break;
}
}
}
}
}
//浏览器的cookie key
private static final String SESSIONID="xyzlycimanage";
//从浏览器获取SessionId
private String getCookieSessionId(Cookie[] cookies){
if(cookies != null){
for(Cookie cookie : cookies){
if(SESSIONID.equals(cookie.getName())){
return cookie.getValue();
}
}
}
return null;
}
}
其实又明白了一件事,那就是基础基础很重要呀,JavaWeb的知识,能够与SpringMVC这种框架完美契合,解决我们使用框架解决不了的事情,更加证明了 所有天上飞的理念,都离不开地基。