SpringBoot ThreadLocal 全局动态变量设置

需求说明:

现有一个游戏后台管理系统,该系统可管理多个大区的数据,但是需要使用大区id实现数据隔离,并且提供了大区选择功能,先择大区后展示对应的数据。需要实现一下几点:

1.前端请求时,area_id是必传的

1.数据隔离,包括查询及增删改:使用mybatis拦截器实现

2.多个用户同时操作互不影响

3.非前端调用场景的处理:定时任务、mq

1.前端决定area_id

为了解决多个用户可以互不影响的使用不同的area_id,因此采用前端传递area_id的方式。前端的area_id可以放在缓存中,调用接口时将数据塞入头部中传给接口,实现了不同浏览器之间area_id互不影响的方式

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

简单来说就是,一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的,即每个线程都只能看到自己线程的值

2.ThreadLocal

接口接收到头部中的area_id后,将其设置到ThreadLocal中,以保证在整个请求的线程中都可以获取到该值。

并且为了防止内存泄漏及数据错乱问题,需要在请求结束时清除ThreadLocal。

3.请求拦截器

使用拦截器实现一下几个步骤:

(1)校验头部area_id,保证请求时改参数必传

(2)对头部area_id的获取、ThreadLocal设置、ThreadLocal清除,这样可以保证每次请求时都会使用头部中area_id

java 复制代码
package org.jeecg.modules.game.config.area;


import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 游戏区id拦截器
 * @author: sxd
 * @date: 2025-02-06 13:56
 **/
@Component
public class AreaIdInterceptor implements HandlerInterceptor {
    @Autowired
    private AreaIdHolder areaIdHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String areaId = request.getHeader(CommonConstant.GAME_AREA_ID);
        if (StringUtils.isEmpty(areaId)) {
            throw new JeecgBootException("请先指定游戏大区");
        }
        areaIdHolder.setAreaId(Long.parseLong(areaId));
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理ThreadLocal,防止内存泄露
        areaIdHolder.remove();
    }

}
java 复制代码
package org.jeecg.modules.game.config.area;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 注册拦截器
 * @author: sxd
 * @date: 2025-02-06 14:14
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private AreaIdInterceptor areaIdInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(areaIdInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/game/area/areaSave", "/game/area/areaServerTree", "/game/common/changeArea");

    }
}
java 复制代码
package org.jeecg.modules.game.config.area;


import org.springframework.stereotype.Component;

/**
 * @author: sxd
 * @date: 2025-02-06 14:18
 **/
@Component
public class AreaIdHolder {
    private static final ThreadLocal<Long> areaIdHolder = new ThreadLocal<>();

    public void setAreaId(Long gameId) {
        areaIdHolder.set(gameId);
    }

    public Long getAreaId() {
        return areaIdHolder.get();
    }

    public void remove() {
        areaIdHolder.remove();
    }
}

4.mybatis拦截器

mybatis plus配置:目的是在指定的数据表操作中,在条件中自动追加条件,即area_id

同时ignoreTable方法中设置无需拦截的数据表。并检测当area_id不存在时,不进行拦截处理,以兼容非前端请求时没有area_id的情况,如定时任务、mq消费

java 复制代码
package org.jeecg.modules.game.config.area;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author sxd
 */
@Configuration
public class MybatisPlusConfig {

    @Autowired
    private GameTenantLineHandler gameTenantLineHandler;

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor1() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(gameTenantLineHandler));
        return interceptor;
    }
}
java 复制代码
package org.jeecg.modules.game.config.area;


import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @author: sxd
 * @date: 2025-01-13 16:06
 **/
@Service
public class GameTableService {
    @Autowired
    private AreaIdHolder gameIdHolder;

    public List<String> getGameModuleTableNames() {
        List<String> tableNames = TableInfoHelper.getTableInfos().stream()
                .map(tableInfo -> tableInfo.getEntityType().getPackage().getName())
                .filter(packageName -> packageName.startsWith("org.jeecg.modules.game.entity"))
                .distinct()
                .flatMap(packageName -> TableInfoHelper.getTableInfos().stream()
                        .filter(tableInfo -> tableInfo.getEntityType().getPackage().getName().equals(packageName))
                        .map(tableInfo -> tableInfo.getTableName()))
                .collect(Collectors.toList());

        return tableNames;
    }
}
java 复制代码
package org.jeecg.modules.game.config.area;

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * @author: sxd
 * @date: 2025-01-13 15:53
 **/
@Component
public class GameTenantLineHandler implements TenantLineHandler {
    @Autowired
    private AreaIdHolder gameIdHolder;

    @Autowired
    private GameTableService gameTableService;

    @Override
    public Expression getTenantId() {
        Long gameId = gameIdHolder.getAreaId();
        if (gameId == null) {
            return null;
        }
        return new LongValue(gameId);
    }

    @Override
    public String getTenantIdColumn() {
        return "area_id";
    }

    /**
     * 返回 true 表示不走AreaId逻辑
     */
    @Override
    public boolean ignoreTable(String tableName) {
        // 没有区域id则不会走自动在where种追加area_id的逻辑
        Long gameId = gameIdHolder.getAreaId();
        if (gameId == null) {
            return true;
        }
        // 忽略不需要添加 area_id 条件的表
        List<String> gameTableNames = gameTableService.getGameModuleTableNames();
        return !gameTableNames.contains(tableName) || Arrays.asList(new String[]{"game_area", "game_prop"}).contains(tableName);
    }
}
相关推荐
Lhappy嘻嘻7 小时前
Java IO|File 文件操作 + 字节流 / 字符流完整笔记 + 递归删除文件实战
java·笔记·php
伊玛目的门徒8 小时前
试用leetcode之典中典 二数之和问题
java·算法·leetcode
懒鸟一枚10 小时前
深入理解 Linux 内存、Swap 交换分区与分页机制的关系
java·linux·数据库
我命由我1234512 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
考虑考虑12 小时前
Sentinel安装
java·后端·微服务
碎碎念_49212 小时前
SpringBoot + Vue 前后端分离从 0 到 1 完整环境配置流程
vue.js·spring boot·后端
凤凰院凶涛QAQ14 小时前
《Java版数据结构 & 集合类剖析》栈与队列:“push/pop 是栈的灵魂,offer/poll 是队列的骨架——四组 API,两种人生”
java·开发语言·数据结构
掘金_答案15 小时前
上线那天,一个 ConcurrentHashMap 差点送走我的 AI 客服——3 天排查 JVM 血泪史
java·后端·架构
猿与禅15 小时前
CosId 分布式 ID 生成器完全教程:从架构原理到生产落地
java·shardingsphere·雪花算法·分布式id·高性能·cosid·号段模式
MindUp15 小时前
企业网盘权限模型解析:多层级访问控制与审计能力选型指南
java·linux·运维