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
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 {@Autowiredprivate AreaIdHolder areaIdHolder;@Overridepublic 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;}@Overridepublic 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 {@Autowiredprivate AreaIdInterceptor areaIdInterceptor;@Overridepublic 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 {@Autowiredprivate GameTenantLineHandler gameTenantLineHandler;@Beanpublic 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 {@Autowiredprivate 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 {@Autowiredprivate AreaIdHolder gameIdHolder;@Autowiredprivate GameTableService gameTableService;@Overridepublic Expression getTenantId() {Long gameId = gameIdHolder.getAreaId();if (gameId == null) {return null;}return new LongValue(gameId);}@Overridepublic String getTenantIdColumn() {return "area_id";}/*** 返回 true 表示不走AreaId逻辑*/@Overridepublic 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);}
}