初始化项目提交,并添加 flowable 7 的 dm 兼容
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 多租户配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "yudao.tenant")
|
||||
@Data
|
||||
public class TenantProperties {
|
||||
|
||||
/**
|
||||
* 租户是否开启
|
||||
*/
|
||||
private static final Boolean ENABLE_DEFAULT = true;
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enable = ENABLE_DEFAULT;
|
||||
|
||||
/**
|
||||
* 需要忽略多租户的请求
|
||||
*
|
||||
* 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
|
||||
*/
|
||||
private Set<String> ignoreUrls = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 需要忽略跨(切换)租户访问的请求
|
||||
*
|
||||
* 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的!
|
||||
*/
|
||||
private Set<String> ignoreVisitUrls = Collections.emptySet();
|
||||
|
||||
/**
|
||||
* 需要忽略多租户的表
|
||||
*
|
||||
* 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
|
||||
*/
|
||||
private Set<String> ignoreTables = Collections.emptySet();
|
||||
|
||||
/**
|
||||
* 需要忽略多租户的 Spring Cache 缓存
|
||||
*
|
||||
* 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
|
||||
*/
|
||||
private Set<String> ignoreCaches = Collections.emptySet();
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.rpc.TenantRequestInterceptor;
|
||||
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
||||
@EnableFeignClients(clients = TenantCommonApi.class) // 主要是引入相关的 API 服务
|
||||
public class YudaoTenantRpcAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantRequestInterceptor tenantRequestInterceptor() {
|
||||
return new TenantRequestInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 忽略租户,标记指定方法不进行租户的自动过滤
|
||||
*
|
||||
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
|
||||
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
|
||||
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
|
||||
*
|
||||
* 特殊:
|
||||
* 1、如果添加到 Controller 类上,则该 URL 自动添加到 {@link TenantProperties#getIgnoreUrls()} 中
|
||||
* 2、如果添加到 DO 实体类上,则它对应的表名“相当于”自动添加到 {@link TenantProperties#getIgnoreTables()} 中
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface TenantIgnore {
|
||||
|
||||
/**
|
||||
* 是否开启忽略租户,默认为 true 开启
|
||||
*
|
||||
* 支持 Spring EL 表达式,如果返回 true 则满足条件,进行租户的忽略
|
||||
*/
|
||||
String enable() default "true";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
|
||||
/**
|
||||
* 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
|
||||
* 例如说,一个定时任务,读取所有数据,进行处理。
|
||||
* 又例如说,读取所有数据,进行缓存。
|
||||
*
|
||||
* 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class TenantIgnoreAspect {
|
||||
|
||||
@Around("@annotation(tenantIgnore)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
|
||||
Boolean oldIgnore = TenantContextHolder.isIgnore();
|
||||
try {
|
||||
// 计算条件,满足的情况下,才进行忽略
|
||||
Object enable = SpringExpressionUtils.parseExpression(tenantIgnore.enable());
|
||||
if (Boolean.TRUE.equals(enable)) {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
}
|
||||
|
||||
// 执行逻辑
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
TenantContextHolder.setIgnore(oldIgnore);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.context;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
|
||||
/**
|
||||
* 多租户上下文 Holder
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantContextHolder {
|
||||
|
||||
/**
|
||||
* 当前租户编号
|
||||
*/
|
||||
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 是否忽略租户
|
||||
*/
|
||||
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 获得租户编号
|
||||
*
|
||||
* @return 租户编号
|
||||
*/
|
||||
public static Long getTenantId() {
|
||||
return TENANT_ID.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常
|
||||
*
|
||||
* @return 租户编号
|
||||
*/
|
||||
public static Long getRequiredTenantId() {
|
||||
Long tenantId = getTenantId();
|
||||
if (tenantId == null) {
|
||||
throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
|
||||
+ DocumentEnum.TENANT.getUrl());
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public static void setTenantId(Long tenantId) {
|
||||
TENANT_ID.set(tenantId);
|
||||
}
|
||||
|
||||
public static void setIgnore(Boolean ignore) {
|
||||
IGNORE.set(ignore);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前是否忽略租户
|
||||
*
|
||||
* @return 是否忽略
|
||||
*/
|
||||
public static boolean isIgnore() {
|
||||
return Boolean.TRUE.equals(IGNORE.get());
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
TENANT_ID.remove();
|
||||
IGNORE.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 拓展多租户的 BaseDO 基类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public abstract class TenantBaseDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 多租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
|
||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantDatabaseInterceptor implements TenantLineHandler {
|
||||
|
||||
/**
|
||||
* 忽略的表
|
||||
*
|
||||
* KEY:表名
|
||||
* VALUE:是否忽略
|
||||
*/
|
||||
private final Map<String, Boolean> ignoreTables = new HashMap<>();
|
||||
|
||||
public TenantDatabaseInterceptor(TenantProperties properties) {
|
||||
// 不同 DB 下,大小写的习惯不同,所以需要都添加进去
|
||||
properties.getIgnoreTables().forEach(table -> {
|
||||
addIgnoreTable(table, true);
|
||||
});
|
||||
// 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
|
||||
addIgnoreTable("DUAL", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
return new LongValue(TenantContextHolder.getRequiredTenantId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
// 情况一,全局忽略多租户
|
||||
if (TenantContextHolder.isIgnore()) {
|
||||
return true;
|
||||
}
|
||||
// 情况二,忽略多租户的表
|
||||
tableName = SqlParserUtils.removeWrapperSymbol(tableName);
|
||||
Boolean ignore = ignoreTables.get(tableName.toLowerCase());
|
||||
if (ignore == null) {
|
||||
ignore = computeIgnoreTable(tableName);
|
||||
synchronized (ignoreTables) {
|
||||
addIgnoreTable(tableName, ignore);
|
||||
}
|
||||
}
|
||||
return ignore;
|
||||
}
|
||||
|
||||
private void addIgnoreTable(String tableName, boolean ignore) {
|
||||
ignoreTables.put(tableName.toLowerCase(), ignore);
|
||||
ignoreTables.put(tableName.toUpperCase(), ignore);
|
||||
}
|
||||
|
||||
private boolean computeIgnoreTable(String tableName) {
|
||||
// 找不到的表,说明不是 yudao 项目里的,不进行拦截(忽略租户)
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
|
||||
if (tableInfo == null) {
|
||||
return true;
|
||||
}
|
||||
// 如果继承了 TenantBaseDO 基类,显然不忽略租户
|
||||
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
|
||||
return false;
|
||||
}
|
||||
// 如果添加了 @TenantIgnore 注解,显然也不忽略租户
|
||||
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
|
||||
return tenantIgnore != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.job;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 多租户 Job 注解
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TenantJob {
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.job;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import com.xxl.job.core.context.XxlJobContext;
|
||||
import com.xxl.job.core.context.XxlJobHelper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 多租户 JobHandler AOP
|
||||
* 任务执行时,会按照租户逐个执行 Job 的逻辑
|
||||
*
|
||||
* 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Aspect
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TenantJobAspect {
|
||||
|
||||
private final TenantFrameworkService tenantFrameworkService;
|
||||
|
||||
@Around("@annotation(tenantJob)")
|
||||
public void around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
|
||||
// 获得租户列表
|
||||
List<Long> tenantIds = tenantFrameworkService.getTenantIds();
|
||||
if (CollUtil.isEmpty(tenantIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 逐个租户,执行 Job
|
||||
Map<Long, String> results = new ConcurrentHashMap<>();
|
||||
AtomicBoolean success = new AtomicBoolean(true); // 标记,是否存在失败的情况
|
||||
XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext(); // XXL-Job 上下文
|
||||
tenantIds.parallelStream().forEach(tenantId -> {
|
||||
// TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
|
||||
TenantUtils.execute(tenantId, () -> {
|
||||
try {
|
||||
XxlJobContext.setXxlJobContext(xxlJobContext);
|
||||
// 执行 Job
|
||||
Object result = joinPoint.proceed();
|
||||
results.put(tenantId, StrUtil.toStringOrEmpty(result));
|
||||
} catch (Throwable e) {
|
||||
results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
|
||||
success.set(false);
|
||||
// 打印异常
|
||||
XxlJobHelper.log(StrUtil.format("[多租户({}) 执行任务({}),发生异常:{}]",
|
||||
tenantId, joinPoint.getSignature(), ExceptionUtils.getStackTrace(e)));
|
||||
}
|
||||
});
|
||||
});
|
||||
// 记录执行结果
|
||||
if (success.get()) {
|
||||
XxlJobHelper.handleSuccess(JsonUtils.toJsonString(results));
|
||||
} else {
|
||||
XxlJobHelper.handleFail(JsonUtils.toJsonString(results));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user