需求說明:
現有一個游戲后臺管理系統,該系統可管理多個大區的數據,但是需要使用大區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);}
}