1 前提springboot整合好 mybatis-plus (版本3.5.4)
需要实现多租户的表,添加修改对应字段和 pojo类 (表添加tenant_id字段,pojo添加tenantId属性)
2 配置文件更改,方便扩展
#多租户配置
tenant:
enable: true
column: tenant_id
ignoreTables:
- tb_order
- users
3 属性文件配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 多租户配置属性类
*/
@Component
@Data
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
/**
* 是否开启多租户
*/
private Boolean enable;
/**
* 租户id字段名
*/
private String column;
/**
* 需要忽略的多租户的表,此配置优先filterTables,若此配置为空则启用filterTables
*/
private List<String> ignoreTables;
}
4 租户id管理类,从threadlocal 中取,这个根据业务情况来
/**
* 租户上下文持有者,用于管理当前线程的租户ID。
* 提供了获取、设置和清除当前线程租户ID的方法。
*/
public class TenantContextHolder {
// 静态ThreadLocal变量,用于存储当前线程的租户ID
static ThreadLocal<Long> tenantThreadLocal = new ThreadLocal<>();
/**
* 获取当前线程的租户ID。
*
* @return 当前线程的租户ID
*/
public static Long getCurrentTenantId() {
return tenantThreadLocal.get();
}
/**
* 设置当前线程的租户ID。
*
* @param tenantId 要设置的租户ID
*/
public static void setCurrentTenantId(Long tenantId) {
tenantThreadLocal.set(tenantId);
}
/**
* 清除当前线程的租户ID,使得线程不再关联任何租户。
*/
public static void clear() {
tenantThreadLocal.remove();
}
}
5 多租户处理器实现,核心类
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.List;
/**
* 多租户处理器实现,核心类
*/
@Component
public class MultiTenantHandler implements TenantLineHandler {
@Autowired
private TenantProperties tenantProperties;
/**
* 根据表名判断是否忽略拼接多租户条件
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
//忽略指定表对租户数据的过滤
List<String> ignoreTables = tenantProperties.getIgnoreTables();
if (null != ignoreTables && ignoreTables.contains(tableName)) {
return true;
}
return false;
}
/**
* 获取租户ID值表达式,只支持单个ID值 (实际应该从用户信息中获取)
* @return 租户ID值表达式
*/
@Override
public Expression getTenantId() {
if(TenantContextHolder.getCurrentTenantId()!=null)
{
Long tenantId = TenantContextHolder.getCurrentTenantId();
if(tenantId!=null)
{
return new LongValue(tenantId);
}
}
//设默认的值 或 异常,灵活处理
return new LongValue(0);
}
/**
* 获取租户字段名,默认字段名叫: tenant_id
* @return 租户字段名
*/
@Override
public String getTenantIdColumn() {
return tenantProperties.getColumn();
}
}
6 mybatis 拦截器配置,核心类
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
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;
@Configuration
public class MybatisPlusConfig {
@Autowired
private MultiTenantHandler tenantHandler;
@Autowired
private TenantProperties tenantProperties;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
if (tenantProperties.getEnable()) {
// 添加多租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantHandler));
}
//分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//防止全表更新与删除插件: BlockAttackInnerInterceptor
BlockAttackInnerInterceptor blockAttackInnerInterceptor = new BlockAttackInnerInterceptor();
interceptor.addInnerInterceptor(blockAttackInnerInterceptor);
return interceptor;
}
}
7 不需要租户的方法 (此注解只能放在mapper层的接口方法中),可以添加 如下配置,如,生成的sql就不会拼接租户的字段了
@InterceptorIgnore(tenantLine = "true")
List<Book> searchAllBook_diySql();
8 写个springboot 拦截器或用其它方案,如拦截器里,将header中的租户id,设置到TenantContextHolder.setCurrentTenantId里面去
9 开始测试,需要使用租户功能的,在header中添加 租户id
在增删改查操作时,sql都会拼接上 租户id的条件
坑坑坑坑坑坑坑坑坑坑坑坑坑坑坑坑坑坑坑坑
1 自己写的sql ,不要加租户id字段,不要加租户id字段,不要加租户id字段,系统会自己拼接sql
2 这个示例,租户id都是统一添加在header中,拦截器再从header中取租户id,并设置到TenantContextHolder.setCurrentTenantId里面去,某些方法又在参数里如 requestParam或RequestBody中设置了租户id,会引发数据错乱,这边是使用了一个aop拦截器,将参数中传递的租户id,统一设为null,如下
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
@Aspect
@Component
public class TenantIdAspect {
@Before("execution(* cn.baidu.sharding_jdbc_test.control..*.*(..)) && args(*)")
public void cleanTenantId(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
// 使用反射来设置租户ID为null
if (arg != null) {
try {
String fieldname = "tenantId"; // 假设所有实体类中租户ID的字段名为tenantId
Field field = arg.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(arg, null);
} catch (NoSuchFieldException | IllegalAccessException e) {
// 处理异常,例如记录日志
e.printStackTrace();
}
}
}
}
}