- 接着学习。之前的博客的进度:完成用户模块的注册接口的开发以及注册时的参数合法性校验、也基本完成用户模块的登录接口的主逻辑的基础上、JWT令牌"的组成与使用、完成了"登录认证"(生成与验证JWT令牌)以及完成获取用户详细信息接口开发。具体往回看了解的链接如下。
- 但是在获取用户详细信息的接口开发的代码还有优化的空间。本篇博客是学习借助ThreadLocal来优化代码。
目录(1)查看UserController类查看之前写的"获取用户详细信息"接口的代码。
(II)提供测试方法"testThreadLocalSetAndGet()",添加注解@Test。
(1)回到UserController层的"/userInfo"接口。
(4)再次回到UserController层的"/userInfo"接口。
(II)重写拦截器中的afterCompletion()方法。
一、问题与分析
(1)查看UserController类查看之前写的"获取用户详细信息"接口的代码。
(2)查看之前在拦截器里面也写过解析token令牌的代码。
(3)问题
- 在其它地方也需要使用用户信息的时候。这个时候代码就不一定只重复一次了。
- 所以既然在拦截器中写了同样的代码,那么在"/userInfo"接口里面不在写了。然后参数也不在声明了、解析token的代码也不写了。
- 而是复用拦截器里面去解析得到的结果。如何做到?ThreadLocal。
二、ThreadLocal
(1)基本作用
- 提供线程局部变量。
- 提供了方法用来存储数据:set()方法、get()方法。
- 使用ThreadLocal存储的数据,线程安全(像局部变量一样。每个线程属于自己,互不影响)
(2)举例
- 如下有一个ThreadLocal对象tl。然后又有两个线程:"蓝色线程"与"绿色线程"。它们都持有ThreadLoca tl这个对象的引用。
- 在这两个线程都能调用set()方法存储用户名。它们分别存储了"萧炎"、"药尘"。
- 在"蓝色线程"中调用get()方法获取名字时,只能获取到"萧炎"。
- 因为ThreadLocal分别为两个线程创建存储数据的空间。可以做到线程隔离。
(3)IDAE中操作演示
(I)创建一个测试类"ThreadLocalTest"。
(II)提供测试方法"testThreadLocalSetAndGet()",添加注解@Test。
(III)完善方法内部。
- 提供一个ThreadLocal对象
- 开启两个线程。
- 开启线程:"new Thread()"。然后调用"start()"方法开启线程。"new Thread()"可以传递两个参数。分别是线程任务与线程名字。而线程任务的Runnable对象用Lambda表达式来给他提供。再用"逗号"后面填写另外一个参数name的值("蓝色"、"绿色")。
- 线程任务:首先第一个线程调用"ThreadLocal对象tl"的set()方法存一个用户名"萧炎"。再调用"get()"方法获取当前线程里面存的用户名。并将获取到用户名输出到控制台。然后第二个线程调用"ThreadLocal对象tl"的set()方法存一个用户名"药尘"。再调用"get()"方法获取当前线程里面存的用户名。并将获取到用户名输出到控制台。
- 为了区分,在输出加一个字符串"Thread.currentThread().getName()",并且输出三次(方便看)
javapackage com.feisi; import org.junit.jupiter.api.Test; public class ThreadLocalTest { @Test public void testThreadLocalSetAndGet(){ //提供一个ThreadLocal对象 ThreadLocal tl = new ThreadLocal(); //开启两个线程 //第一个线程 new Thread(()->{ //Lambda表达式写线程任务 tl.set("萧炎"); System.out.println(Thread.currentThread().getName()+":"+tl.get()); System.out.println(Thread.currentThread().getName()+":"+tl.get()); System.out.println(Thread.currentThread().getName()+":"+tl.get()); },"蓝色").start(); //第二个线程 new Thread(()->{ //Lambda表达式写线程任务 tl.set("药尘"); System.out.println(Thread.currentThread().getName()+":"+tl.get()); System.out.println(Thread.currentThread().getName()+":"+tl.get()); System.out.println(Thread.currentThread().getName()+":"+tl.get()); },"绿色").start(); } }
Lambda表达式。
- Lambda表达式由参数列表、箭头符号('->')和方法体组成。其中方法体既可以是一个表达式,也可以是一个语句块。
- 其中,表达式会被执行,然后返回执行结果;语句块中的语句会被依次执行,就像方法中的语句一样。
(IIII)测试。
- 注意。现在用的是同一个ThreadLocal对象来存储和获取数据。所以要测试:重点查看"蓝色"线程获取的是不是"萧炎",而"绿色"线程获取的是不是"药尘"。
- 最后发现它会两个线程都开辟了存储空间,做到了线程隔离。
- 因为线程分配随机执行,所以执行顺序不一定有序。
(4)联系与思考
(I)初步解决方法
- 我们可以维护一个全局的ThreadLocal对象,用来存储用户名、用户id这类数据。
- 我们可以在请求到达拦截器之后,调用这个ThreadLocal对象的set()方法来存储用户的id。
- 然后当请求到达Controller、Service、Dao层的时候,它们的方法内部只要有需要,就可以调用tl.get()方法获取到用户id,然后去使用。
(II)问题
- Controller、Service、Dao它们一般在容器中是单例的。当获取用户id的时候,怎么让它们知道当前需要获取用户的id是哪个?会不会发生线程安全的问题?(get()方法获取id)
- 举例,有两个用户去访问该程序。他们携带的userId分别为"1"和"2"。当请求到达Tomcat时,服务器会为每一个用户开辟一个线程,用来提供服务。
- 补充。将Controller、Service、Dao设计为单例可以显著提高系统的性能和效率。但需要注意线程安全性问题。如果组件中包含状态信息或共享资源,则需要采取适当的措施来确保线程安全。
(III)结论
- 通过分析,可以大致得出借助ThreadLocal可以做两件事情。
- 第一件事情。减少参数的传递,方法中的参数不需要重复声明了。
- 第二件事情。可以在同一个线程的执行代码间,进行共享数据。比如把拦截器中的数据,把它共享到Controller、Service、Dao层等进行使用。
(接下来,借助ThreadLocal将之前写的代码进行优化。)
三、ThreadLocal优化用户详细信息接口。
(回到IDEA中)
(1)回到UserController层的"/userInfo"接口。
- 注释掉之前写的方法参数(请求头)。
- 注释掉token解析代码。
(2)使用一个ThreadLocal工具类。
- 为了使用方便,我使用了一个ThreadLocal的工具类。将类复制到utils包下。
- 首先看到下面的工具类。它提供了一个常量"THREAD_LOCAL"。这个就是用来维护一个全局唯一的ThreadLocal对象。
- 然后有一个get()方法,把得到的数据返回回去。而且还是用的是一个泛型。(如果声明的是String,它会强转为String。声明的Map,强转Map)因为ThreadLocal可以存储任意类型的数据。
- 还提供了一个set()方法,存储值调用set()方法。
- 注意:提供了一个remove()方法,它就是调用了ThreadLocal中的remove()方法。它是用来清除之前存的数据。因为ThreadLocal设定的时候是唯一的(全局变量),那么它的生命周期特别的长。如果用完了不清除,就会一直驻留,可能造成内存泄漏问题。
javapackage com.feisi.utils; import java.util.HashMap; import java.util.Map; /** * ThreadLocal 工具类 */ @SuppressWarnings("all") public class ThreadLocalUtil { //提供ThreadLocal对象, private static final ThreadLocal THREAD_LOCAL = new ThreadLocal(); //根据键获取值 public static <T> T get(){ return (T) THREAD_LOCAL.get(); } //存储键值对 public static void set(Object value){ THREAD_LOCAL.set(value); } //清除ThreadLocal 防止内存泄漏 public static void remove(){ THREAD_LOCAL.remove(); } }
(3)回到拦截器LoginInterceptor中。
- 在解析得到的业务数据后,将业务数据存储到ThreadLocal中。
- 利用到上面的ThreadLocal工具类。
(4)再次回到UserController层的"/userInfo"接口。
- 刚刚在拦截器存储的值是Map类型的。
- 现在通过get()方法获取,并用变量接收即可。
- 再获取指定的属性"username"即可。
java@GetMapping("/userInfo") public Result<User> userInfo(/*@RequestHeader(name = "Authorization") String token*/){ //方法内部根据用户名查询用户 /* Map<String, Object> map = JwtUtil.parseToken(token); String username = map.get("username").toString();*/ Map<String,Object> map = ThreadLocalUtil.get(); String username = map.get("username").toString(); //拿到username就去调用service层的据用户名查询用户方法 User user = userService.findByName(username); return Result.success(user); }
(5)重启工程进行接口测试。
- 注意。登录认证生成的"JWT令牌"可能测试时已经过期,之前设定的是12个小时。所以在postman中测试接口时,需要重新生成一个"JWT令牌",然后在去统一设置请求头。
- 再次测试接口成功获取数据。
(访问"localhost:8080/user/userInfo")
(6)问题。
(I)分析。
- 当我们用完数据要记得去清除这个数据。
- 应该在哪个位置去清楚???(remove()方法)
- 分析:拦截器中,解析成功携带的token令牌后,将它存储到ThreadLocal中。当请求放行了(return了true)之后。在Controller、Service、Dao层中都可以使用到这个共享数据。当响应完成了之后,也就是这一次请求结束了就不再使用了。
- 所以我们应该是请求完成了,然后把数据清除掉。
(II)重写拦截器中的afterCompletion()方法。
java@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //清空ThreadLocal中的数据 //防止内存泄漏 ThreadLocalUtil.remove(); }
(III)整个拦截器的代码
javapackage com.feisi.interceptors; import com.feisi.pojo.Result; import com.feisi.utils.JwtUtil; import com.feisi.utils.ThreadLocalUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Map; @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //令牌验证 String token = request.getHeader("Authorization"); //解析token //用提供的工具类解析和验证token try { Map<String, Object> claims = JwtUtil.parseToken(token); //将得到的业务数据存储到ThreadLocal中 ThreadLocalUtil.set(claims); //放行 return true; } catch (Exception e) { //设置http响应状态码为401 response.setStatus(401); //不放行 return false; } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //清空ThreadLocal中的数据 //防止内存泄漏 ThreadLocalUtil.remove(); } }
(7)再次重启工程进行接口测试。没啥问题。
四、总结
(1)使用ThreadLocal需要注意的地方。
- 用来存取数据:set()/get()方法。
- 使用ThreadLocal存储的数据,是线程安全的。
- 用完之后一定记得remove()方法释放。防止内存泄漏!