一,问题提出:
通常在项目设计开发中,用户登录后,我们会将用户的信息存到session,如果想在其它地方获取session中的用户信息,我们需要先获取HttpServletRequest,再通过request.getSession得到HttpSession。例如下代码:
public static User getSessionUser(HttpServletRequest request) { if(request.getSession().getAttribute( "sessionuser" ) != null) { return (User)request.getSession().getAttribute( "sessionuser" ); } return null; }
但是这样操作会很麻烦并且存在并发问题
1.1 每次获取session中的用户信息,先手动获取到HttpServletRequest对象。
每次要获取session都要传递Request请求参数,尤其是service层或者dao层也要使用到user的信息,而通常在一个大型项目中,service层和dao层都是和web层分离开来,
都是单独的工程,不依赖servlet api,大家也不会为了在service层或者dao层获取登录用户信息而这么做,这样显得会很奇怪,所以我们只能在action中调用service的时候,将用户信息以参数形式传过去。
对于session中的用户信息,我们不仅想要在action中随用随取,还想在其它普通类中取,即使不依赖servlet api, 我们也要在方法里随用随取,我们在处理请求的时候,很多操作都要获取当前用户的ID等信息,由上可见,我们凡是在action的方法中任何一处想要获取session中的用户信息,则必须要先手动获取到HttpServletRequest,是不是比较麻烦
1.2 如果遇到高并发,多人同时登录系统时,会出现session混乱。
考虑到并发,如果两个人或者多人同时登录系统,A置成自己的session了,B又置成他的session了,两人开始打架了。
二,解决方案
客户端发送的每次http请求,对应的服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程。
我们引入ThreadLocal,首先它不是用来解决多线程并发共享一类问题的,它解决的是在一个线程中参数的传递。
2.1 ThreadLocal
顾名思义,就是本地线程,可是这个名字实在容易让人误解,因为其实它是本地线程局部变量的意思,首先我们要知道,我们每个请求都会对应一个线程,
这个ThreadLocal就是这个线程使用过程中的一个变量,该变量为其所属线程所有,各个线程互不影响。
这里我们要了解一下ThreadLocal的三个方法:
ThreadLocal.set(T value); //设置值 ThreadLocal.get(); //获取值 ThreadLocal.remove(); //移除值
具体实例:
package com.tigerhhzz.wuaimai.common; /** * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id */ public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); /** * 设置值 * @param id */ public static void setCurrentId(Long id){ threadLocal.set(id); } /** * 获取值 * @return */ public static Long getCurrentId(){ return threadLocal.get(); } }
所以我们可以借助这个ThreadLocal来存储登录用户的信息,在一个请求中,所有调用的方法都在同一个线程中去处理,这样就实现了在任何地方都可以获取到用户信息了,从而摆脱了HttpServletRequest的束缚。
2.2 避免了跨层之间的参数传递,实现了层与层之间的松耦合。
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal为每个线程提供了单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
2.3 项目实际应用
项目结构如下:
BaseContext .class
package com.tigerhhzz.wuaimai.common; /** * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id */ public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); /** * 设置值 * @param id */ public static void setCurrentId(Long id){ threadLocal.set(id); } /** * 获取值 * @return */ public static Long getCurrentId(){ return threadLocal.get(); } }
logincheckfilter部分代码:
//4、判断登录状态,如果已登录,则直接放行 if(request.getSession().getAttribute("employee") != null){ log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee")); Long empId = (Long) request.getSession().getAttribute("employee"); BaseContext.setCurrentId(empId); filterChain.doFilter(request,response); return; }
我们可以在logincheckfilter的dofilter方法中获取当前登录用户id,并调用ThreadLocal方法来设置当前线程的线程局部变量的值(用户id),然后在mymetaobjecthandler的update方法中调用threadlocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
MyMetaObjecthandler 公共字段填充类
package com.tigerhhzz.wuaimai.common; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; /** * 自定义元数据对象处理器 */ @Component @Slf4j public class MyMetaObjecthandler implements MetaObjectHandler { /** * 插入操作,自动填充 * @param metaObject */ @Override public void insertFill(MetaObject metaObject) { log.info("公共字段自动填充[insert]..."); log.info(metaObject.toString()); metaObject.setValue("createTime", LocalDateTime.now()); metaObject.setValue("updateTime",LocalDateTime.now()); metaObject.setValue("createUser", BaseContext.getCurrentId()); metaObject.setValue("updateUser",BaseContext.getCurrentId()); } /** * 更新操作,自动填充 * @param metaObject */ @Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充[update]..."); log.info(metaObject.toString()); long id = Thread.currentThread().getId(); log.info("线程id为:{}",id); metaObject.setValue("updateTime",LocalDateTime.now()); metaObject.setValue("updateUser",BaseContext.getCurrentId()); } }