以前用过ruoyi的框架,基于mybatis-plus实现多租户,后来接触Mybatis-flex。多租户思路按照plus做走了很多弯路,写下来记录下。其实mybatis-flex实现多租户很简单,一切围绕
@Column(tenantId=ture)和TenantFaction来做工作就行了。
先说下目的
1.要可以对指定url实现多忽略多租户,因为程序是注册邀请制,所以登录时用户是不需要多租户验证的,所以对一些列登录url如auth/**要可以实现自动忽略
2.可以随时使用注解@TenantIgnore忽略方法多租户,其实mybatis-flex也提供了忽略方法,可是还是喜欢注解,而且自己实现更灵活
3.可以在执行特定方法时,以某个租户的身份去执行(如定时任务)
基于以上3点,需要如下几个文件来实现
1.上下文类,存储当前租户及可用性
/**
* 租户上下文
*/
public class TenantContext {
private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
private static final ThreadLocal<Boolean> IGNORE_TENANT = ThreadLocal.withInitial(()->false);
/**
* 设置租户ID
* @param tenantId
*/
public static void setCurrentTenant(Long tenantId) {
CURRENT_TENANT.set(tenantId);
}
/**
* 获取租户ID
* @return
*/
public static Long getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
IGNORE_TENANT.remove();
}
/**
* 设置是否忽略租户验证
*/
public static void setIgnore(boolean ignore) {
IGNORE_TENANT.set(ignore);
}
/**
* 是否忽略租户
* @return
*/
public static boolean isIgnore(){
return IGNORE_TENANT.get();
}
}
2.请求过滤TenantFilter,主要过滤url请求,设置租户上下文,具体业务场景,根据自己情况调整
/**
* 租户请求过滤
* 主要用于初始化 租户相关信息、租户角色认证、权限过滤等
*/
@Component(index = 1)
@Slf4j
public class TenantFilter implements Filter {
@Inject("${tenant.path-ignore}")
private List<String> ignorePathList;
@Inject("${tenant.enable}")
private Boolean tenantEnable;
@Inject
private SysUserApi sysUserApi;
@Inject
private SysTenantApi sysTenantApi;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
// 0. 放行OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(ctx.method())) {
chain.doFilter(ctx);
return;
}
SaBaseLoginUser userInfo = null;
// 检查是否匹配忽略路径
boolean shouldIgnore = ignorePathList.stream()
.anyMatch(pattern -> pathMatcher.match(pattern, ctx.path()));
if(!tenantEnable || shouldIgnore){
TenantContext.setIgnore(true);
chain.doFilter(ctx);
return ;
}
//1.得到header中的租户ID
Long tenantId = ConvertUtils.toLong(ctx.header(TENANT_ID_HEADER),0L);
if(tenantId.compareTo(0L)<=0){
log.info("[TenantFilter] -> 此次请求未携带租户id ip:{},method:{},uri:{}", ctx.realIp(), ctx.method(), ctx.uri());
ctx.render(CommonResult.error("请携带租户ID:Tenant-Id"));
return;
}
TenantContext.setCurrentTenant(tenantId);
userInfo = StpLoginUserUtil.getLoginUser();
if (userInfo == null) {
ctx.render(CommonResult.error("未登录"));
return;
}
if (!userInfo.getTenantId().equals(tenantId)) {
log.info("[TenantFilter] -> 无该租户访问权限user_id:{},tenant_id:{} ip:{},method:{},uri:{}", userInfo.getId(), tenantId, ctx.realIp(), ctx.method(), ctx.uri());
ctx.render(CommonResult.error("无权访问此租户信息"));
return;
}
if(tenantId.equals(TenantConstants.TENANT_SYSTEM_ID))
{
chain.doFilter(ctx); //直接放行
return;
}
//验证租户状态
SysTenantDTO tenant = sysTenantApi.getTenant(tenantId);
if(tenant==null){
ctx.render(CommonResult.error("不存在的租户"));
return;
}
if(tenant.getExpireTime().getTime()>= DateUtils.getTimestampMillis()){
ctx.render(CommonResult.error("您的账户所属单位已过期"));
return;
}
chain.doFilter(ctx);
}
}
3.租户Aop,使用solon的拦截器Interceptor 实现,结合注解TenantIgnore可实现对指定方法及其调用的其他子方法忽略租户
@Component
public class TenantAspect implements Interceptor {
@Inject("${tenant.enable}")
private boolean tenantEnable;
/**
* 设置忽略多租户
* @param methodAnno
* @param classAnno
* @return
*/
private boolean ignoreTenant(TenantIgnore methodAnno, TenantIgnore classAnno) {
if(!tenantEnable){
return true; //未开启
}
if(TenantContext.isIgnore()){
return true;
}
TenantContext.setIgnore(methodAnno !=null || classAnno!=null);
return TenantContext.isIgnore(); // 有忽略注解,则跳过
}
@Override
public Object doIntercept(Invocation inv) throws Throwable {
//1.读取忽略注解
TenantIgnore methodAnnotation = inv.getMethodAnnotation(TenantIgnore.class);
TenantIgnore classAnnotation = inv.getTargetAnnotation(TenantIgnore.class);
//2.忽略租户过滤
ignoreTenant(methodAnnotation,classAnnotation);
return inv.invoke();
}
}
4.多租户忽略注解@TenantIgnore,使用@Inherited可以实现注解向下传递给调用的其他方法
@Around(TenantAspect.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface TenantIgnore {
}
5.TenantFactory实现,getTenantIds中返回的是一个数字,这里如果上下文忽略租户,就直接返回一个空数组,这样mybatis-flex就不会过滤租户了
public class YnTenantFactory implements TenantFactory {
@Inject("${tenant.table-ignore}")
private List<String> ignoreTableList;
@Override
public Object[] getTenantIds() {
if(TenantContext.isIgnore() || TenantContext.getCurrentTenant()==null){
return new Object[0];
}
return new Object[]{TenantContext.getCurrentTenant()};
}
}
6.注册TenantFactory
@Configuration
public class TenConfigure {
@Bean
public TenantFactory tenantFactory(){
TenantFactory tenantFactory = new YnTenantFactory();
return tenantFactory;
}
}
7.执行工具,这个是参考的yudao框架实现的
public class TenantUtils {
/**
* 使用指定租户执行方法
*
* 制定后将不再忽略租户,执行完成后恢复原状态
*
* @param tenantId 租户编号
* @param runnable 要执行的方法
*/
public static void execute(Long tenantId, Runnable runnable) {
Long oldTenantId = TenantContext.getCurrentTenant();
Boolean oldIgnore = TenantContext.isIgnore();
try {
TenantContext.setCurrentTenant(tenantId);
TenantContext.setIgnore(false);
// 执行逻辑
runnable.run();
} finally {
TenantContext.setCurrentTenant(oldTenantId);
TenantContext.setIgnore(oldIgnore);
}
}
/**
* 使用指定租户,执行对应的逻辑
*
* 制定后将不再忽略租户,执行完成后恢复原状态
*
* @param tenantId 租户编号
* @param callable 要执行的方法
*/
public static <V> V execute(Long tenantId, Callable<V> callable) {
Long oldTenantId = TenantContext.getCurrentTenant();
Boolean oldIgnore = TenantContext.isIgnore();
try {
TenantContext.setCurrentTenant(tenantId);
TenantContext.setIgnore(false);
// 执行逻辑
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
TenantContext.setCurrentTenant(oldTenantId);
TenantContext.setIgnore(oldIgnore);
}
}
/**
* 忽略租户执行方法,一般用于多租户后台数据定时任务执行。
* 如数据分拣、聚合等
*
* @param runnable 逻辑
*/
public static void executeIgnore(Runnable runnable) {
Boolean oldIgnore = TenantContext.isIgnore();
try {
TenantContext.setIgnore(true);
// 执行逻辑
runnable.run();
} finally {
TenantContext.setIgnore(oldIgnore);
}
}
/**
* 将多租户编号,添加到 header 中
*
* @param headers HTTP 请求 headers
* @param tenantId 租户编号
*/
public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
if (tenantId != null) {
headers.put(TenantConstants.TENANT_ID_HEADER, tenantId.toString());
}
}
}
写法比较简单,记录下,下边简单介绍下使用方法
1.mybatis-flex中,只要给字段增加@Column(tenantId=true)就可以启用多租户的过滤条件,所以只需要在需要进行租户过滤的entity进行设置就行了
@ApiModelProperty(value = "租户号", position = 29)
@Column(tenantId = true)
private Long tenantId;
2.忽略多租户,因为@TenantIgnore使用了@Inherited,可以向下传导,所以我们只需要在最顶级方法中设置即可
/*可以是一个api*/
@TenantIgnore
@Mapping("/iot/device/page")
public CommonResult<PageResult<IotDevicePageResult>> page(IotDevicePageParam iotDevicePageParam) {
return CommonResult.data(iotDeviceService.page(iotDevicePageParam));
}
/*u也可以是一个方法api*/
@TenantIgnore
public Long add(@Validated IotDeviceAddParam iotDeviceAddParam) {}



1626

被折叠的 条评论
为什么被折叠?



