springboot实战学习(10)(ThreadLoacl优化获取用户详细信息接口)(重写拦截器afterCompletion()方法)

news2024/10/1 17:44:07
  • 接着学习。之前的博客的进度:完成用户模块的注册接口的开发以及注册时的参数合法性校验、也基本完成用户模块的登录接口的主逻辑的基础上、JWT令牌"的组成与使用、完成了"登录认证"(生成与验证JWT令牌)以及完成获取用户详细信息接口开发。具体往回看了解的链接如下。

springboot实战学习(9)(配置mybatis“驼峰命名“和“下划线命名“自动转换)(postman接口测试统一添加请求头)(获取用户详细信息接口)_springboot 的实体类实现下划线命名?-CSDN博客文章浏览阅读760次,点赞10次,收藏23次。这篇博客主要是完成用户模块的“获取用户详细信息”接口开发。其中包括读取请求头中的“JWT令牌”并解析获取用户名、在postman接口测试统一添加请求头以及在.yml文件中配置mybatis"驼峰命名"和"下划线命名"自动转换..._springboot 的实体类实现下划线命名?https://blog.csdn.net/m0_74363339/article/details/142531404?spm=1001.2014.3001.5501

  • 但是在获取用户详细信息的接口开发的代码还有优化的空间。本篇博客是学习借助ThreadLocal来优化代码。

目录

一、问题与分析

(1)查看UserController类查看之前写的"获取用户详细信息"接口的代码。

(2)查看之前在拦截器里面也写过解析token令牌的代码。

(3)问题

二、ThreadLocal

(1)基本作用

(2)举例

(3)IDAE中操作演示

(I)创建一个测试类"ThreadLocalTest"。

(II)提供测试方法"testThreadLocalSetAndGet()",添加注解@Test。

(III)完善方法内部。

Lambda表达式。

(IIII)测试。

(4)联系与思考

(I)初步解决方法

(II)问题

(III)结论

三、ThreadLocal优化用户详细信息接口。

(1)回到UserController层的"/userInfo"接口。

(2)使用一个ThreadLocal工具类。

(3)回到拦截器LoginInterceptor中。

(4)再次回到UserController层的"/userInfo"接口。

(5)重启工程进行接口测试。

(6)问题。

(I)分析。

(II)重写拦截器中的afterCompletion()方法。

(III)整个拦截器的代码

(7)再次重启工程进行接口测试。没啥问题。

四、总结

(1)使用ThreadLocal需要注意的地方。


一、问题与分析

(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()",并且输出三次(方便看)
package 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设定的时候是唯一的(全局变量),那么它的生命周期特别的长。如果用完了不清除,就会一直驻留,可能造成内存泄漏问题。
package 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"即可。
  @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()方法。
@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清空ThreadLocal中的数据
        //防止内存泄漏
        ThreadLocalUtil.remove();
    }
(III)整个拦截器的代码
package 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()方法释放。防止内存泄漏!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2182840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【源码部署】vue项目nvm安装(Windows篇)

nvm node version manager&#xff08;node版本管理工具&#xff09; 通过将多个node 版本安装在指定路径&#xff0c;然后通过 nvm 命令切换时&#xff0c;就会切换我们环境变量中 node 命令指定的实际执行的软件路径。 使用场景&#xff1a;比如我们手上同时在做好几个项目&a…

C# HttpClient请求URL重定向后丢失Authorization认证头

搜查官方文档后发现&#xff1a; HttpWebRequest.AllowAutoRedirect Property (System.Net) | Microsoft Learn 微软提供的http类库HttpClient &#xff08;HttpWebRequest\WebClient已不推荐使用&#xff0c;用HttpClient代替&#xff09;有备注提醒&#xff1a;当使用自动重…

B站字幕提取方法

1.获取json文件内容 1.点击F12进入开发者模式&#xff0c;选择网络模块&#xff1b; 2.输入关键字&#xff0c;例如json、ai_subtitle、subtitle等&#xff1b; 3.点击视频下方的字幕功能&#xff0c;开启&#xff1b;再点击响应单元&#xff0c;复制内容&#xff1b; 2.去jso…

好玩的水表电表

<!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>水表电表</title><style>* {margin:…

视频——教学篇——12——定一个涨粉小目标,如何从0-10万粉?

文章目录 1、粉丝即正义。什么是粉丝价值&#xff1f;粉丝价值粉丝活跃度商业价值 2、找到账号目标和定位3、涨粉的基础是更新频率4、优质少更与良品多更的策略5、有播放却不涨粉&#xff1f;如何提高播放转粉率&#xff1f; 1、粉丝即正义。什么是粉丝价值&#xff1f; 在了解…

CTMO时代下的营销新力量:2+1链动模式AI智能名片商城小程序

在当今这个瞬息万变的商业世界里&#xff0c;营销领域正经历着一场深刻的变革。传统的CMO岗位似乎在时代的浪潮中逐渐失去了它的光芒&#xff0c;CTMO正在悄然取代传统CMO的岗位。 随着营销丛林现象的出现&#xff0c;企业面临着前所未有的挑战。许多企业发现&#xff0c;那些传…

【RockyLinux 9.4】CentOS也可以用。安装教程(使用U盘,避免踩坑简略版本)

一、制作一个镜像安装盘 1.下载镜像&#xff08;本教程使用9.4版本&#xff09; 官网&#xff1a; https://rockylinux.org/zh-CN 2.使用 UltraISO&#xff0c;制作写入硬盘镜像 二、调整相关参数&#xff0c;准备进入安装流程 1.关闭 Secure Boot&#xff08;BIOS 里面关…

【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)

文章目录 从零实现 list 容器&#xff1a;细粒度剖析与代码实现前言1. list 的核心数据结构1.1节点结构分析&#xff1a; 2. 迭代器设计与实现2.1 为什么 list 需要迭代器&#xff1f;2.2 实现一个简单的迭代器2.2.1 迭代器代码实现&#xff1a;2.2.2 解释&#xff1a; 2.3 测试…

【C++打怪之路Lv5】-- 类和对象(下)

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;C打怪之路&#xff0c;python从入门到精通&#xff0c;数据结构&#xff0c;C语言&#xff0c;C语言题集&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持创作博文(平均质量分82)&#…

基于MTK7981平台,学习了解理解SoC上电和boot流程

当SoC芯片流片回来后&#xff0c;并不是直接通电就可以使用的。需要进行上电复位&#xff0c;然后对SoC进行配置使其进入到正常工作状态&#xff0c;然后才能运行正常的程序和任务。这是一个比较复杂的过程&#xff0c;也是在芯片设计阶段&#xff0c;SoC需要重点考虑的问题。上…

机器人的性能指标

1. 负荷能力 负荷能力负荷能力是指机器人在满足其他性能要求的情况下,能够承载的负荷重量。例如,一台机器人的最大负荷能力可能远大于它的额定负荷能力,但是达到最大负荷时,机器人的工作精度可能会降低,可能无法准确地沿着预定的轨迹运动,或者产生额外的偏差。机器人的负荷量与…

如何确定光纤用几芯 用光纤与网线区别在哪里

光纤用几芯&#xff1f; 光纤芯数&#xff0c;主要和光纤连接的设备接口和设备的通信方式有关。一般来说&#xff0c;光纤中光芯的数量&#xff0c;为设备接口总数乘以2后&#xff0c;再加上10%&#xff5e;20&#xff05;的备用数量&#xff0c;而如果设备的通信方式有设备多…

Linux数据备份

1、Linux服务器中哪些数据需要备份 1&#xff09;Linux系统重要数据&#xff1a; ①/root/目录&#xff0c;管理员家目录 ②/home/目录&#xff0c;普通用户家目录 ③/etc/目录 &#xff0c;系统重要的配置文件保存目录 2&#xff09;安装服务的数据&#xff1a;例apache①…

新160个crackme -070-CodeFantasy-crackme

运行分析 需破解用户名和注册码点击注册无反应 PE分析 Delphi程序&#xff0c;32位&#xff0c;无壳 静态分析&动态调试 ida找到关键字符串&#xff0c;进入函数 动态调试关键函数&#xff0c;逻辑&#xff1a;对Names进行sub_408A68函数加密得到v14&#xff0c;若v14与Ser…

<<迷雾>> 第5章 从逻辑学到逻辑电路(4)--或门及其符号 示例电路

info::操作说明 鼠标单击开关切换开合状态 系统中使用一个类似箭头的形状表示或门 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhttps://book.xiaogd.net/cyjsjdmw-examples/assets/circuit/cyjsjdmw-ch05-13-or-gate.txt 原图

使用TiDB企业版Lightning导入ORC文件到TiDB

作者&#xff1a; 数据源的TiDB学习之路 原文来源&#xff1a; https://tidb.net/blog/818f84f0 TiDB Lightning 是用于从静态文件导入 TB 级数据到 TiDB 集群的工具&#xff0c;常用于 TiDB 集群的初始化数据导入。在开源社区版本中&#xff0c;TiDB Lightning 支持以下文件…

疾风大模型气象,基于大模型预测未来天气的探索

引言 天气预测一直是科学领域的重要课题&#xff0c;影响着农业、航空、交通等多个行业。传统的天气预报依赖于数值天气预报&#xff08;Numerical Weather Prediction, NWP&#xff09;模型&#xff0c;这些模型基于物理定律和历史数据来模拟大气运动。然而&#xff0c;随着数…

LUCEDA IPKISS Tutorial 74:布尔运算去掉部分图层

案例分享&#xff1a;通过布尔运算&#xff0c;将版图部分图层挖空 所有代码如下&#xff1a; from si_fab import all as pdk from ipkiss3 import all as i3 import numpy as npclass grating_coupler(i3.PCell):_name_prefix "grating_coupler"r i3.Positive…

Spring Task 2024/9/30

Spring Task是Spring框架提供的任务调度工具&#xff0c;可以按照约定时间自动执行某个代码逻辑。 作用&#xff1a;定时自动执行某段java代码。 cron表达式 在线Cron表达式生成器 (qqe2.com)&#x1f448;在线生成网站 入门案例 SkyApplication 启动类 package com.sky;im…

对于 Vue CLI 项目如何引入Echarts以及动态获取数据

&#x1f680;个人主页&#xff1a;一颗小谷粒 &#x1f680;所属专栏&#xff1a;Web前端开发 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 1、数据画卷—Echarts介绍 1.1 什么是Echarts&#xff1f; 1.2 Echarts官网地址 2、Vue CLI 项目…