• ruoyi-nbcio-plus基于vue3的多租户机制


    更多ruoyi-nbcio功能请看演示系统

    gitee源代码地址

    前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio

    演示地址:RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/

    更多nbcio-boot功能请看演示系统 

    gitee源代码地址

    后端代码: https://gitee.com/nbacheng/nbcio-boot

    前端代码:https://gitee.com/nbacheng/nbcio-vue.git

    在线演示(包括H5) : http://122.227.135.243:9888

           因为基于ruoyi-vue-plus的框架,所以多租户总体基于使用了 MyBatis-Plus (简称 MP)的多租户插件功能

          可以参考

        实现主要有以下步骤:

        在相关表添加多租户字段
        在多租户配置TenantConfig 里中添加多租户插件拦截器 TenantLineInnerInterceptor
    根据业务对多租户插件拦截器 TenantLineInnerInterceptor 进行配置(多租户字段、需要进行过滤的表等)
        在数据库相关表中加入租户id字段 tenant_id(别忘了相关实体类也要加上)

    具体代码如下:

    1. @EnableConfigurationProperties(TenantProperties.class)
    2. @AutoConfiguration(after = {RedisConfig.class, MybatisPlusConfig.class})
    3. @ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
    4. public class TenantConfig {
    5. /**
    6. * 初始化租户配置
    7. */
    8. @Bean
    9. public boolean tenantInit(MybatisPlusInterceptor mybatisPlusInterceptor,
    10. TenantProperties tenantProperties) {
    11. List interceptors = new ArrayList<>();
    12. // 多租户插件 必须放到第一位
    13. interceptors.add(tenantLineInnerInterceptor(tenantProperties));
    14. interceptors.addAll(mybatisPlusInterceptor.getInterceptors());
    15. mybatisPlusInterceptor.setInterceptors(interceptors);
    16. return true;
    17. }
    18. /**
    19. * 多租户插件
    20. */
    21. public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties tenantProperties) {
    22. return new TenantLineInnerInterceptor(new PlusTenantLineHandler(tenantProperties));
    23. }
    24. @Bean
    25. public RedissonAutoConfigurationCustomizer tenantRedissonCustomizer(RedissonProperties redissonProperties) {
    26. return config -> {
    27. TenantKeyPrefixHandler nameMapper = new TenantKeyPrefixHandler(redissonProperties.getKeyPrefix());
    28. SingleServerConfig singleServerConfig = ReflectUtils.invokeGetter(config, "singleServerConfig");
    29. if (ObjectUtil.isNotNull(singleServerConfig)) {
    30. // 使用单机模式
    31. // 设置多租户 redis key前缀
    32. singleServerConfig.setNameMapper(nameMapper);
    33. ReflectUtils.invokeSetter(config, "singleServerConfig", singleServerConfig);
    34. }
    35. ClusterServersConfig clusterServersConfig = ReflectUtils.invokeGetter(config, "clusterServersConfig");
    36. // 集群配置方式 参考下方注释
    37. if (ObjectUtil.isNotNull(clusterServersConfig)) {
    38. // 设置多租户 redis key前缀
    39. clusterServersConfig.setNameMapper(nameMapper);
    40. ReflectUtils.invokeSetter(config, "clusterServersConfig", clusterServersConfig);
    41. }
    42. };
    43. }
    44. /**
    45. * 多租户缓存管理器
    46. */
    47. @Primary
    48. @Bean
    49. public CacheManager tenantCacheManager() {
    50. return new TenantSpringCacheManager();
    51. }
    52. /**
    53. * 多租户鉴权dao实现
    54. */
    55. @Primary
    56. @Bean
    57. public SaTokenDao tenantSaTokenDao() {
    58. return new TenantSaTokenDao();
    59. }
    60. }

    其中 自定义租户处理器代码如下:

    1. /**
    2. * 自定义租户处理器
    3. *
    4. * @author nbacheng
    5. */
    6. @Slf4j
    7. @AllArgsConstructor
    8. public class PlusTenantLineHandler implements TenantLineHandler {
    9. private final TenantProperties tenantProperties;
    10. @Override
    11. public Expression getTenantId() {
    12. String tenantId = TenantHelper.getTenantId();
    13. if (StringUtils.isBlank(tenantId)) {
    14. log.error("无法获取有效的租户id -> Null");
    15. return new NullValue();
    16. }
    17. String dynamicTenantId = TenantHelper.getDynamic();
    18. if (StringUtils.isNotBlank(dynamicTenantId)) {
    19. // 返回动态租户
    20. return new StringValue(dynamicTenantId);
    21. }
    22. // 返回固定租户
    23. return new StringValue(tenantId);
    24. }
    25. @Override
    26. public boolean ignoreTable(String tableName) {
    27. String tenantId = TenantHelper.getTenantId();
    28. // 判断是否有租户
    29. if (StringUtils.isNotBlank(tenantId)) {
    30. // 不需要过滤租户的表
    31. List excludes = tenantProperties.getExcludes();
    32. // 非业务表
    33. List tables = ListUtil.toList(
    34. "gen_table",
    35. "gen_table_column"
    36. );
    37. tables.addAll(excludes);
    38. return tables.contains(tableName);
    39. }
    40. return true;
    41. }
    42. }

    上面就是重载了mybasisplus的TenantLineHandler 

    1. /**
    2. * 租户处理器( TenantId 行级 )
    3. *
    4. * @author hubin
    5. * @since 3.4.0
    6. */
    7. public interface TenantLineHandler {
    8. /**
    9. * 获取租户 ID 值表达式,只支持单个 ID 值
    10. *

    11. *
    12. * @return 租户 ID 值表达式
    13. */
    14. Expression getTenantId();
    15. /**
    16. * 获取租户字段名
    17. *

    18. * 默认字段名叫: tenant_id
    19. *
    20. * @return 租户字段名
    21. */
    22. default String getTenantIdColumn() {
    23. return "tenant_id";
    24. }
    25. /**
    26. * 根据表名判断是否忽略拼接多租户条件
    27. *

    28. * 默认都要进行解析并拼接多租户条件
    29. *
    30. * @param tableName 表名
    31. * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
    32. */
    33. default boolean ignoreTable(String tableName) {
    34. return false;
    35. }
    36. /**
    37. * 忽略插入租户字段逻辑
    38. *
    39. * @param columns 插入字段
    40. * @param tenantIdColumn 租户 ID 字段
    41. * @return
    42. */
    43. default boolean ignoreInsert(List columns, String tenantIdColumn) {
    44. return columns.stream().map(Column::getColumnName).anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));
    45. }
    46. }

    多租户插件的调用流程如下图:

    上面主要是用到了mybatisPlusInterceptor,

    1. public class MybatisPlusInterceptor implements Interceptor {
    2. @Setter
    3. private List interceptors = new ArrayList<>();
    4. @Override
    5. public Object intercept(Invocation invocation) throws Throwable {
    6. Object target = invocation.getTarget();
    7. Object[] args = invocation.getArgs();
    8. if (target instanceof Executor) {
    9. final Executor executor = (Executor) target;
    10. Object parameter = args[1];
    11. boolean isUpdate = args.length == 2;
    12. MappedStatement ms = (MappedStatement) args[0];
    13. if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
    14. RowBounds rowBounds = (RowBounds) args[2];
    15. ResultHandler resultHandler = (ResultHandler) args[3];
    16. BoundSql boundSql;
    17. if (args.length == 4) {
    18. boundSql = ms.getBoundSql(parameter);
    19. } else {
    20. // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
    21. boundSql = (BoundSql) args[5];
    22. }
    23. for (InnerInterceptor query : interceptors) {
    24. if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
    25. return Collections.emptyList();
    26. }
    27. query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    28. }
    29. CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
    30. return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
    31. } else if (isUpdate) {

    上面进入beforeQuery

    1. public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    2. if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
    3. return;
    4. }
    5. PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
    6. mpBs.sql(parserSingle(mpBs.sql(), null));
    7. }

    通过parserSingle的processParser

    1. public String parserSingle(String sql, Object obj) {
    2. if (logger.isDebugEnabled()) {
    3. logger.debug("original SQL: " + sql);
    4. }
    5. try {
    6. Statement statement = JsqlParserGlobal.parse(sql);
    7. return processParser(statement, 0, sql, obj);
    8. } catch (JSQLParserException e) {
    9. throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e.getCause(), sql);
    10. }
    11. }
    1. protected String processParser(Statement statement, int index, String sql, Object obj) {
    2. if (logger.isDebugEnabled()) {
    3. logger.debug("SQL to parse, SQL: " + sql);
    4. }
    5. if (statement instanceof Insert) {
    6. this.processInsert((Insert) statement, index, sql, obj);
    7. } else if (statement instanceof Select) {
    8. this.processSelect((Select) statement, index, sql, obj);
    9. } else if (statement instanceof Update) {
    10. this.processUpdate((Update) statement, index, sql, obj);
    11. } else if (statement instanceof Delete) {
    12. this.processDelete((Delete) statement, index, sql, obj);
    13. }
    14. sql = statement.toString();
    15. if (logger.isDebugEnabled()) {
    16. logger.debug("parse the finished SQL: " + sql);
    17. }
    18. return sql;
    19. }

    通过这个processSelect的进入select


    1. @Override
    2. protected void processSelect(Select select, int index, String sql, Object obj) {
    3. final String whereSegment = (String) obj;
    4. processSelectBody(select.getSelectBody(), whereSegment);
    5. List withItemsList = select.getWithItemsList();
    6. if (!CollectionUtils.isEmpty(withItemsList)) {
    7. withItemsList.forEach(withItem -> processSelectBody(withItem, whereSegment));
    8. }
    9. }

    其中进入processSelectBody处理

    1. protected void processSelectBody(SelectBody selectBody, final String whereSegment) {
    2. if (selectBody == null) {
    3. return;
    4. }
    5. if (selectBody instanceof PlainSelect) {
    6. processPlainSelect((PlainSelect) selectBody, whereSegment);
    7. } else if (selectBody instanceof WithItem) {
    8. WithItem withItem = (WithItem) selectBody;
    9. processSelectBody(withItem.getSubSelect().getSelectBody(), whereSegment);
    10. } else {
    11. SetOperationList operationList = (SetOperationList) selectBody;
    12. List selectBodyList = operationList.getSelects();
    13. if (CollectionUtils.isNotEmpty(selectBodyList)) {
    14. selectBodyList.forEach(body -> processSelectBody(body, whereSegment));
    15. }
    16. }
    17. }

    之后进入processPlainSelect

    1. protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {
    2. //#3087 github
    3. List selectItems = plainSelect.getSelectItems();
    4. if (CollectionUtils.isNotEmpty(selectItems)) {
    5. selectItems.forEach(selectItem -> processSelectItem(selectItem, whereSegment));
    6. }
    7. // 处理 where 中的子查询
    8. Expression where = plainSelect.getWhere();
    9. processWhereSubSelect(where, whereSegment);
    10. // 处理 fromItem
    11. FromItem fromItem = plainSelect.getFromItem();
    12. List list = processFromItem(fromItem, whereSegment);
    13. List
    14. mainTables = newArrayList<>(list);
    15. // 处理 join
    16. List joins = plainSelect.getJoins();
    17. if (CollectionUtils.isNotEmpty(joins)) {
    18. mainTables = processJoins(mainTables, joins, whereSegment);
    19. }
    20. // 当有 mainTable 时,进行 where 条件追加
    21. if (CollectionUtils.isNotEmpty(mainTables)) {
    22. plainSelect.setWhere(builderExpression(where, mainTables, whereSegment));
    23. }
    24. }
    25. 上面进入builderExpression 构造表达式

      1. protected Expression builderExpression(Expression currentExpression, List
      tables, final String whereSegment) {
    26. // 没有表需要处理直接返回
    27. if (CollectionUtils.isEmpty(tables)) {
    28. return currentExpression;
    29. }
    30. // 构造每张表的条件
    31. List expressions = tables.stream()
    32. .map(item -> buildTableExpression(item, currentExpression, whereSegment))
    33. .filter(Objects::nonNull)
    34. .collect(Collectors.toList());
    35. // 没有表需要处理直接返回
    36. if (CollectionUtils.isEmpty(expressions)) {
    37. return currentExpression;
    38. }
    39. // 注入的表达式
    40. Expression injectExpression = expressions.get(0);
    41. // 如果有多表,则用 and 连接
    42. if (expressions.size() > 1) {
    43. for (int i = 1; i < expressions.size(); i++) {
    44. injectExpression = new AndExpression(injectExpression, expressions.get(i));
    45. }
    46. }
    47. 上面的buildTableExpression加入了租户的条件

      1. public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
      2. if (tenantLineHandler.ignoreTable(table.getName())) {
      3. return null;
      4. }
      5. return new EqualsTo(getAliasColumn(table), tenantLineHandler.getTenantId());
      6. }

      最终通过前面的processParser获取select的sql表达式,加入了多租户条件。

    48. 相关阅读:
      vue3导出表格(导出成Execl表)
      【LeetCode】145. 二叉树的后序遍历 [ 左子树 右子树 根结点]
      【图像分类】2021-CvT
      flex设置为1后为什么要设置width为0,和布局超出省略号为什么会超出容器,为什么会没有用
      【Spring】Spring(IoC、AOP、Bean生命周期、事务、三级缓存、源码)面试题
      数组中 forEach 和 Map 的区别
      【Kotlin精简】第6章 反射
      PASCAL VOC2012 数据集讲解与制作自己的数据集
      永磁同步电机滞环电流控制(PI双闭环)matlab仿真模型
      01 基于yum方式部署Kubernetes集群
    49. 原文地址:https://blog.csdn.net/qq_40032778/article/details/137949972