当前位置: 首页 > article >正文

TransmittableThreadLocal维护Token中的userId

许多 Web 应用中,Token(通常是JWT)用于身份验证和授权。每次请求到达后,我们需要从 Token 中提取出用户的身份信息(如 userId),并在请求的生命周期内维护这个信息。一个常见的做法是使用 ThreadLocal 来存储 userId,确保每个请求线程都有自己的用户信息。

1. ThreadLocal

ThreadLocal是 Java 提供的一个用于在多线程环境下存储线程局部变量的类。每个线程都可以独立地存取 ThreadLocal 变量,而不会相互干扰。它是处理多线程并发问题的一种简单有效的方式,常用于存储每个线程独有的数据。

1.1. ThreadLocal 的工作原理

  • 每个线程都会持有一个独立的 ThreadLocal 变量副本,线程之间无法共享这个副本。
  • 通过 ThreadLocal.get()ThreadLocal.set() 方法,可以在当前线程中读取或设置该变量。
  • ThreadLocal 的数据在当前线程执行期间是隔离的,当线程结束时,ThreadLocal 变量会被自动清除。

但是ThreadLocal在异步场景下是无法给子线程共享父线程中创建的线程副本数据的,也就是说当前线程的子线程无法获取到当前线程ThreadLocal中的值。 尤其是使用 CompletableFutureExecutorService 或类似的异步任务执行框架时,父线程(例如请求线程)需要在异步回调或者后续的任务中传递上下文信息(如 userIdtraceId 等),以便在整个操作链中能够追踪到同一请求的上下文。

1.2. 问题展示

假设我们使用 Java 的 ExecutorService 来提交异步任务,而在这些任务中需要访问当前线程的 ThreadLocal 数据(如用户 ID)。

  • 普通 ThreadLocal 示例(会丢失数据)
private static ThreadLocal<String> TL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL.set("parent id");
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(() -> {
            // 子线程无法访问父线程的 ThreadLocal 数据
            System.out.println(TL.get()); // null 
        });
    }

  • 使用 TransmittableThreadLocal 示例(数据能够传递)
private static TransmittableThreadLocal<String> TTL = new TransmittableThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TTL.set("parent id");
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(() -> {
            // 子线程可以访问父线程的 ThreadLocal 数据
            System.out.println(TTL.get()); // 输出 ParentThreadData
        });
    }

结论

  • 在普通 ThreadLocal 示例中,子线程无法访问父线程的数据。
  • 使用 TransmittableThreadLocal 后,子线程能够继承父线程的 ThreadLocal 数据。

2. TransmittableThreadLocal维护Token中的userId

TransmittableThreadLocal 是由阿里巴巴开源的一个项目(TTL)提供的,称为 transmittable-thread-local。 TTL是一个解决多线程和异步编程中 ThreadLocal 数据传递问题的工具。它特别适用于线程池和异步任务执行的场景,能够确保父线程中的 ThreadLocal 数据在子线程(例如异步任务执行的线程)中可用。

2.1. TransmittableThreadLocal工具类

开发项目总对于一些工具、引入的组件服务,我们自己封装上工具类或者是Service能够帮助我们以后更好的维护、管理项目。如果这些工具或者是组件发生了变动,不需要对业务代码进行修改,只需要对我们自己封装得工具类或者是组件进行调整就行。

这里是我针对TTL封装的工具类。

import cn.hutool.core.util.StrUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * {@code ThreadLocalUtil} 是一个工具类,用于管理线程本地存储的数据。
 * 该类使用 {@link TransmittableThreadLocal} 进行线程局部存储,允许在异步操作中传递数据。
 * 它可以为每个线程维护一个 {@link Map},并通过键值对存储对象。
 * 支持线程内存储的值在子线程中传递,适用于跨线程的任务数据传递。
 * <p>
 * 类中的所有操作都是线程安全的,采用了 {@link ConcurrentHashMap} 作为内部存储。
 * </p>
 */
public class ThreadLocalUtil {

    // 使用 TransmittableThreadLocal 来保证跨线程传递数据
    private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();

    /**
     * 设置指定键的值到线程本地存储中,如果值为 {@code null},则设置为 {@link StrUtil#EMPTY}。
     *
     * @param key   存储的键
     * @param value 存储的值,可以为 {@code null}
     */
    public static void set(String key, Object value) {
        Map<String, Object> map = getLocalMap();
        map.put(key, value == null ? StrUtil.EMPTY : value);
    }

    /**
     * 获取指定键对应的值。如果该键不存在,则返回 {@code null}。
     *
     * @param key   要获取值的键
     * @param clazz 值的类型,用于类型转换
     * @param <T>   值的类型
     * @return 返回值,类型为 {@code T},如果键不存在则返回 {@code null}
     */
    public static <T> T get(String key, Class<T> clazz) {
        Map<String, Object> map = getLocalMap();
        return (T) map.getOrDefault(key, null);
    }

    /**
     * 获取当前线程的本地存储的 {@link Map} 实例。如果线程本地存储尚未初始化,则创建并返回一个新的 {@link ConcurrentHashMap}。
     *
     * @return 当前线程的本地 {@link Map} 实例
     */
    public static Map<String, Object> getLocalMap() {
        Map<String, Object> map = THREAD_LOCAL.get();
        if (map == null) {
            map = new ConcurrentHashMap<>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }

    /**
     * 移除当前线程的本地存储。清理线程本地存储中的数据。防止内存泄露
     */
    public static void remove() {
        THREAD_LOCAL.remove();
    }
}

2.2. 在controller之前对TTL进行set

这里我选择的是在拦截器中,从token中获取用户的userID,然后通过ThreadLocalUtil将用户的信息存进TTL。也可以选择网关,但是在微服务中,网关通常都是单独的一个服务,和我们的应用基本上不在一个端口上。

@Component
public class TokenInterceptor  implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    @Value("${jwt.secret}")
    private String secret; // 从哪个服务的配置文件中读取,取决于bean对象交给了哪个服务的spring容器进行管理

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = getToken(request); // 从请求头中获取token
        Long userId = tokenService.getUserId(token, secret);
        ThreadLocalUtil.set(Constants.TTL_KEY_USER_ID, userId);
        // 执行其他业务
        //.....
        return true;
    }

    private String getToken(HttpServletRequest request){
        String token = request.getHeader(HttpConstants.AUTHENTICATION);
        if(StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)){
            token = token.replaceFirst(HttpConstants.PREFIX, "");
        }
        return token;
    }
}

2.3. 需要获取到Token中userID的地方直接get

比如,我们在service中需要根据token得到用户的userId,然后根据userID继续业务绑定。这时候就不用一次次的解析token,特别是针对使用JWT的token,因为为了保护JWT的载荷中的用户隐私信息,通常都会只在载荷中只存放一个key,真实的数据信息,存放到redis中。使用TTL直接就可以获取到该线程下用户的id了。十分的方便。

@Override
    public int enter(String token, ExamDTO examDTO) {
        Exam exam = examMapper.selectById(examDTO.getExamId());
        if(exam == null){
            throw new ServiceException(ResultCode.FAILED_NOT_EXISTS);
        }
        if(exam.getStartTime().isBefore(LocalDateTime.now())){
            throw new ServiceException(ResultCode.EXAM_STARTED);
        }
        // 判断用户是否已经报过名
        //Long userId = tokenService.getUserId(token, secret);
        Long userId = ThreadLocalUtil.get(Constants.TTL_KEY_USER_ID, Long.class);
        UserExam userExam = userExamMapper.selectOne(new LambdaQueryWrapper<UserExam>()
                .eq(UserExam::getExamId, examDTO.getExamId())
                .eq(UserExam::getUserId, userId));
        if(userExam != null){
            throw new ServiceException(ResultCode.USER_EXAM_HAS_ENTER);
        }
        // 添加缓存
        examCacheManager.addUserExamCache(userId, examDTO.getExamId());
        // 用户报名 添加进user_exam表
        userExam = new UserExam();
        userExam.setUserId(userId);
        userExam.setExamId(examDTO.getExamId());
        return userExamMapper.insert(userExam);
    }


http://www.kler.cn/a/419003.html

相关文章:

  • FreeRTOS之vTaskStartScheduler实现分析
  • Ubuntu24.04初始化教程(包含基础优化、ros2)
  • Android 14之HIDL转AIDL通信
  • 架构师的英文:Architect
  • Web 端网站后台裁剪功能:提升图像管理效率的利器
  • Linux - nfs服务器
  • Hexo博客在多个设备同步
  • 数据库原理-期末复习基础知识第二弹
  • 【深度学习】四大图像分类网络之VGGNet
  • 【MySQL】数据库的基本认识和使用
  • 什么是sfp,onu,​为什么PON(​俗称“光猫”​)模块使用SC光纤接口
  • 数据同步、流计算全面强化,TDengine 3.3.4.3 版本正式发布
  • C++高阶算法[汇总]
  • How to monitor Spring Boot apps with the AppDynamics Java Agent
  • Android下载出现open failed: EPERM (Operation not permitted)
  • Vue3 子路由vue如何调用父路由vue中的方法?
  • android 项目多电脑共用github及github项目迁移
  • 嵌入式面试八股文(十)·FreeRTOS相关题目
  • react 响应式变量定义
  • Flutter简单实现滑块验证
  • 基于java+SpringBoot+Vue的教学辅助平台设计与实现
  • arcgis for js点击聚合要素查询其包含的所有要素
  • 30.100ASK_T113-PRO 用QT编写视频播放器(一)
  • OpenGauss数据库介绍
  • 详解 Qt QtPDF之QPdfPageNavigator 页面跳转
  • leetcode3250. 单调数组对的数目 I,仅需1s