SpringBoot ThreadLocal 全局动态变量设置

发布于:2025-07-12 ⋅ 阅读:(21) ⋅ 点赞:(0)

需求说明:

现有一个游戏后台管理系统,该系统可管理多个大区的数据,但是需要使用大区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

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();
    }

}

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");

    }
}

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消费

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;
    }
}
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;
    }
}

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);
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到