功能要求

Mybatis-plus配置多租户。
从header中的token获取租户信息;
所有userController的数据库操作*不要*配置多租户,所有工作计划相关的数据库操作*需要*配置多租户,达成数据隔离。

开发工具:IDEA
技术:Springboot+MabtisPlus+Postman

1、什么是多租户?

多租户技术(多重租赁技术,简称saas),是一种软件架构技术;可以实现同一套程序下用户数据的隔离。

2、实现方案

简单看:就是通过在sql的where条件语句拼接租户id,获取和操作对应租户的数据记录

2.1 添加依赖

<!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.0</version>
        </dependency>

2.2 从header的token中获取租户信息

这里采用 自定义上下文组件传递租户id

2.2.1 自定义上下文

package com.example.demo.config;

import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author
 * @Date 2022/8/2 11:26
 * @Description 自定义上下文
 */

@Component
public class MyContext {
    private static final String KEY_CURRENT_TENANT_ID="KEY_CURRENT_PROVIDER_ID";
    private static final Map<String,Object> M_CONTEXT=new ConcurrentHashMap<>();

    public void setCurrentTenantId(String tenantId){
        M_CONTEXT.put(KEY_CURRENT_TENANT_ID,tenantId);
    }

    public String getCurrentTenantId(){
        return (String) M_CONTEXT.get(KEY_CURRENT_TENANT_ID);
    }
}

2.2.1 获取租户信息到上下文中

在上篇博文的拦截器的token校验成功位置进行添加。
校验成功,获取相应租户信息;

jwtUtils.verify(token);//验证token
    //获取租户信息到上下文中
    Map<String,Claim> claims= jwtUtils.parseJwtTo(token);
    String userId=claims.get("userId").asString();
    myContext.setCurrentTenantId(userId);
    return true;  //上一句无异常就 放行

2.3 自定义实现租户处理器

实现步骤:
在需要隔离的数据表上增添租户id——>实现TenantHandler接口(已废弃,现在用TenantLineHandler)并实现它的方法
(当前数据库有两张表,分别为sys_user和userworkplan,此次就是仅对userworkplan配置多租户;两张表中共有字段userId,所以不需额外添加tenant_id字段,可直接使用userId作为租户功能使用)
package com.example.demo.common;

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.example.demo.config.MyContext;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.stereotype.Component;

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

/**
 * @Author
 * @Date 2022/8/2 10:07
 * @Description 自定义实现租户处理器
 */

@Component
@Slf4j
public class MyTenantHandler implements TenantLineHandler {
    private static final List<String> IGNORE_TENANT_TABLES = Arrays.asList("`sys_user`");//不配置多租户功能的表;这里需要注意表名不要写错,是否需要``根据自己所写的sql语句来定
    //sql语句中``的作用就是与数据库的关键字区分

    /**
     * 获取租户 ID 值表达式,只支持单个 ID 值
     * @return 租户 ID 值表达式
     */
    @Override
    public Expression getTenantId() {
        // 1.从上下文中获取租户ID
        // 2.可以通过过滤器从请求中获取对应租户id
        // 3.从header中的token获取租户信息(✔)——>1
        MyContext myContext=new MyContext();
        String tenantId = myContext.getCurrentTenantId();
        log.info("当前租户ID:{}", tenantId);
        if (Objects.isNull(tenantId)) {
            return new NullValue();
        }
        return new StringValue(tenantId);
    }

    /**
     * 获取租户字段名
     * 默认字段名叫: tenant_id
     * 数据库中字段用的userId
     * @return 租户字段名
     */
    @Override
    public String getTenantIdColumn() {
         return "userId";
    }

    /**
     * 根据表名判断是否忽略拼接多租户条件
     * 默认都要进行解析并拼接多租户条件
     *
     * @param tableName 表名
     * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
     */
    @Override
    public boolean ignoreTable(String tableName) {
        return IGNORE_TENANT_TABLES.stream().anyMatch(ignoreTableName -> ignoreTableName.equalsIgnoreCase(tableName));
    }
}

2.4 进行mybatisplus的配置

package com.example.demo.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.example.demo.common.MyTenantHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Objects;

/**
 * @Author 
 * @Date 2022/8/2 9:59
 * @Description mybatis plus配置
 */

@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = {"com.example.demo.mapper"})
public class MybatisPlusConfiguration {
    /**
     * 配置拦截器(包括分页拦截器)
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 多租户拦截器
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MyTenantHandler()));
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;
    }
    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }
}

3、学习到的知识点

  1. @Component实现bean的注入
  2. 多租户技术的简单实现:①自定义租户处理器 ②进行mybatisplus配置
  3. log.info()打印日志
  4. Claim数据格式
  5. Java打印正在执行的sql语句到控制台方法
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl