初始化提交

This commit is contained in:
YangJ
2024-03-20 09:42:17 +08:00
commit 72f30209cf
3705 changed files with 285827 additions and 0 deletions

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>多租户</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-job</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mq</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.framework.tenant.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Collections;
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 = Collections.emptySet();
/**
* 需要忽略多租户的表
*
* 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
*/
private Set<String> ignoreTables = Collections.emptySet();
}

View File

@ -0,0 +1,132 @@
package cn.iocoder.yudao.framework.tenant.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect;
import cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
import cn.iocoder.yudao.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
import cn.iocoder.yudao.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager;
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Objects;
@AutoConfiguration
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
@EnableConfigurationProperties(TenantProperties.class)
public class YudaoTenantAutoConfiguration {
@Bean
public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) {
return new TenantFrameworkServiceImpl(tenantApi);
}
// ========== AOP ==========
@Bean
public TenantIgnoreAspect tenantIgnoreAspect() {
return new TenantIgnoreAspect();
}
// ========== DB ==========
@Bean
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
MybatisPlusInterceptor interceptor) {
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
// 添加到 interceptor 中
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
MyBatisUtils.addInterceptor(interceptor, inner, 0);
return inner;
}
// ========== WEB ==========
@Bean
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantContextWebFilter());
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
return registrationBean;
}
// ========== Security ==========
@Bean
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
WebProperties webProperties,
GlobalExceptionHandler globalExceptionHandler,
TenantFrameworkService tenantFrameworkService) {
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
globalExceptionHandler, tenantFrameworkService));
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
return registrationBean;
}
// ========== MQ ==========
@Bean
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
return new TenantRedisMessageInterceptor();
}
@Bean
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
return new TenantRabbitMQInitializer();
}
@Bean
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
public TenantRocketMQInitializer tenantRocketMQInitializer() {
return new TenantRocketMQInitializer();
}
// ========== Job ==========
@Bean
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService);
}
// ========== Redis ==========
@Bean
@Primary // 引入租户时tenantRedisCacheManager 为主 Bean
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration,
YudaoCacheProperties yudaoCacheProperties) {
// 创建 RedisCacheWriter 对象
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration);
}
}

View File

@ -0,0 +1,18 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
import java.lang.annotation.*;
/**
* 忽略租户,标记指定方法不进行租户的自动过滤
*
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
*
* @author 芋道源码
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TenantIgnore {
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
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 {
TenantContextHolder.setIgnore(true);
// 执行逻辑
return joinPoint.proceed();
} finally {
TenantContextHolder.setIgnore(oldIgnore);
}
}
}

View File

@ -0,0 +1,79 @@
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();
}
/**
* 获得租户编号 String
*
* @return 租户编号
*/
public static String getTenantIdStr() {
Long tenantId = getTenantId();
return StrUtil.toStringOrNull(tenantId);
}
/**
* 获得租户编号。如果不存在,则抛出 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();
}
}

View File

@ -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;
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.framework.tenant.core.db;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import java.util.HashSet;
import java.util.Set;
/**
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
*
* @author 芋道源码
*/
public class TenantDatabaseInterceptor implements TenantLineHandler {
private final Set<String> ignoreTables = new HashSet<>();
public TenantDatabaseInterceptor(TenantProperties properties) {
// 不同 DB 下,大小写的习惯不同,所以需要都添加进去
properties.getIgnoreTables().forEach(table -> {
ignoreTables.add(table.toLowerCase());
ignoreTables.add(table.toUpperCase());
});
// 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
ignoreTables.add("DUAL");
}
@Override
public Expression getTenantId() {
return new LongValue(TenantContextHolder.getRequiredTenantId());
}
@Override
public boolean ignoreTable(String tableName) {
return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
|| CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
}
}

View File

@ -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 {
}

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.framework.tenant.core.job;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
/**
* 多租户 JobHandler AOP
* 任务执行时,会按照租户逐个执行 Job 的逻辑
*
* 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
*
* @author 芋道源码
*/
@Aspect
@RequiredArgsConstructor
@Slf4j
public class TenantJobAspect {
private final TenantFrameworkService tenantFrameworkService;
@Around("@annotation(tenantJob)")
public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) {
// 获得租户列表
List<Long> tenantIds = tenantFrameworkService.getTenantIds();
if (CollUtil.isEmpty(tenantIds)) {
return null;
}
// 逐个租户,执行 Job
Map<Long, String> results = new ConcurrentHashMap<>();
tenantIds.parallelStream().forEach(tenantId -> {
// TODO 芋艿:先通过 parallel 实现并行1多个租户是一条执行日志2异常的情况
TenantUtils.execute(tenantId, () -> {
try {
joinPoint.proceed();
} catch (Throwable e) {
results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
}
});
});
return JsonUtils.toJsonString(results);
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类
*
* Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器
*
* @author 芋道源码
*/
@Slf4j
public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 添加 TenantKafkaProducerInterceptor 拦截器
try {
String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES);
if (StrUtil.isEmpty(value)) {
value = TenantKafkaProducerInterceptor.class.getName();
} else {
value += "," + TenantKafkaProducerInterceptor.class.getName();
}
environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value);
} catch (NoClassDefFoundError ignore) {
// 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
}
}
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.header.Headers;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import java.util.Map;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类
*
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
*
* @author 芋道源码
*/
public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> {
@Override
public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes());
}
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* 多租户的 RabbitMQ 初始化器
*
* @author 芋道源码
*/
public class TenantRabbitMQInitializer implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RabbitTemplate) {
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor());
}
return bean;
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类
*
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
*
* @author 芋道源码
*/
public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId);
}
return message;
}
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.framework.tenant.core.mq.redis;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* 多租户 {@link AbstractRedisMessage} 拦截器
*
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
*
* @author 芋道源码
*/
public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
@Override
public void sendMessageBefore(AbstractRedisMessage message) {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
message.addHeader(HEADER_TENANT_ID, tenantId.toString());
}
}
@Override
public void consumeMessageBefore(AbstractRedisMessage message) {
String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
if (StrUtil.isNotEmpty(tenantIdStr)) {
TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
}
}
@Override
public void consumeMessageAfter(AbstractRedisMessage message) {
// 注意Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
TenantContextHolder.clear();
}
}

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import org.apache.rocketmq.client.hook.ConsumeMessageContext;
import org.apache.rocketmq.client.hook.ConsumeMessageHook;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import java.util.List;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类
*
* Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现
*
* @author 芋道源码
*/
public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook {
@Override
public String hookName() {
return getClass().getSimpleName();
}
@Override
public void consumeMessageBefore(ConsumeMessageContext context) {
// 校验,消息必须是单条,不然设置租户可能不正确
List<MessageExt> messages = context.getMsgList();
Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size());
// 设置租户编号
String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID);
if (StrUtil.isNotEmpty(tenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(tenantId));
}
}
@Override
public void consumeMessageAfter(ConsumeMessageContext context) {
TenantContextHolder.clear();
}
}

View File

@ -0,0 +1,53 @@
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl;
import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* 多租户的 RocketMQ 初始化器
*
* @author 芋道源码
*/
public class TenantRocketMQInitializer implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DefaultRocketMQListenerContainer) {
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
initTenantConsumer(container.getConsumer());
} else if (bean instanceof RocketMQTemplate) {
RocketMQTemplate template = (RocketMQTemplate) bean;
initTenantProducer(template.getProducer());
}
return bean;
}
private void initTenantProducer(DefaultMQProducer producer) {
if (producer == null) {
return;
}
DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl();
if (producerImpl == null) {
return;
}
producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook());
}
private void initTenantConsumer(DefaultMQPushConsumer consumer) {
if (consumer == null) {
return;
}
DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl();
if (consumerImpl == null) {
return;
}
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import org.apache.rocketmq.client.hook.SendMessageContext;
import org.apache.rocketmq.client.hook.SendMessageHook;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类
*
* Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
*
* @author 芋道源码
*/
public class TenantRocketMQSendMessageHook implements SendMessageHook {
@Override
public String hookName() {
return getClass().getSimpleName();
}
@Override
public void sendMessageBefore(SendMessageContext sendMessageContext) {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
return;
}
sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString());
}
@Override
public void sendMessageAfter(SendMessageContext sendMessageContext) {
}
}

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
/**
* 多租户的 {@link RedisCacheManager} 实现类
*
* 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀
*
* @author airhead
*/
@Slf4j
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
public Cache getCache(String name) {
// 如果开启多租户,则 name 拼接租户后缀
if (!TenantContextHolder.isIgnore()
&& TenantContextHolder.getTenantId() != null) {
name = name + ":" + TenantContextHolder.getTenantId();
}
// 继续基于父方法
return super.getCache(name);
}
}

View File

@ -0,0 +1,117 @@
package cn.iocoder.yudao.framework.tenant.core.security;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* 多租户 Security Web 过滤器
* 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
* 2. 如果请求未带租户的编号,检查是否是忽略的 URL否则也不允许访问。
* 3. 校验租户是合法,例如说被禁用、到期
*
* @author 芋道源码
*/
@Slf4j
public class TenantSecurityWebFilter extends ApiRequestFilter {
private final TenantProperties tenantProperties;
private final AntPathMatcher pathMatcher;
private final GlobalExceptionHandler globalExceptionHandler;
private final TenantFrameworkService tenantFrameworkService;
public TenantSecurityWebFilter(TenantProperties tenantProperties,
WebProperties webProperties,
GlobalExceptionHandler globalExceptionHandler,
TenantFrameworkService tenantFrameworkService) {
super(webProperties);
this.tenantProperties = tenantProperties;
this.pathMatcher = new AntPathMatcher();
this.globalExceptionHandler = globalExceptionHandler;
this.tenantFrameworkService = tenantFrameworkService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
Long tenantId = TenantContextHolder.getTenantId();
// 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user != null) {
// 如果获取不到租户编号,则尝试使用登陆用户的租户编号
if (tenantId == null) {
tenantId = user.getTenantId();
TenantContextHolder.setTenantId(tenantId);
// 如果传递了租户编号,则进行比对租户编号,避免越权问题
} else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
user.getTenantId(), user.getId(), user.getUserType(),
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
"您无权访问该租户的数据"));
return;
}
}
// 如果非允许忽略租户的 URL则校验租户是否合法
if (!isIgnoreUrl(request)) {
// 2. 如果请求未带租户的编号,不允许访问。
if (tenantId == null) {
log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
"请求的租户标识未传递,请进行排查"));
return;
}
// 3. 校验租户是合法,例如说被禁用、到期
try {
tenantFrameworkService.validTenant(tenantId);
} catch (Throwable ex) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
ServletUtils.writeJSON(response, result);
return;
}
} else { // 如果是允许忽略租户的 URL若未传递租户编号则默认忽略租户编号避免报错
if (tenantId == null) {
TenantContextHolder.setIgnore(true);
}
}
// 继续过滤
chain.doFilter(request, response);
}
private boolean isIgnoreUrl(HttpServletRequest request) {
// 快速匹配,保证性能
if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
return true;
}
// 逐个 Ant 路径匹配
for (String url : tenantProperties.getIgnoreUrls()) {
if (pathMatcher.match(url, request.getRequestURI())) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.framework.tenant.core.service;
import java.util.List;
/**
* Tenant 框架 Service 接口,定义获取租户信息
*
* @author 芋道源码
*/
public interface TenantFrameworkService {
/**
* 获得所有租户
*
* @return 租户编号数组
*/
List<Long> getTenantIds();
/**
* 校验租户是否合法
*
* @param id 租户编号
*/
void validTenant(Long id);
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.framework.tenant.core.service;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.time.Duration;
import java.util.List;
/**
* Tenant 框架 Service 实现类
*
* @author 芋道源码
*/
@RequiredArgsConstructor
public class TenantFrameworkServiceImpl implements TenantFrameworkService {
private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException();
private final TenantApi tenantApi;
/**
* 针对 {@link #getTenantIds()} 的缓存
*/
private final LoadingCache<Object, List<Long>> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<Object, List<Long>>() {
@Override
public List<Long> load(Object key) {
return tenantApi.getTenantIdList();
}
});
/**
* 针对 {@link #validTenant(Long)} 的缓存
*/
private final LoadingCache<Long, ServiceException> validTenantCache = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<Long, ServiceException>() {
@Override
public ServiceException load(Long id) {
try {
tenantApi.validateTenant(id);
return SERVICE_EXCEPTION_NULL;
} catch (ServiceException ex) {
return ex;
}
}
});
@Override
@SneakyThrows
public List<Long> getTenantIds() {
return getTenantIdsCache.get(Boolean.TRUE);
}
@Override
public void validTenant(Long id) {
ServiceException serviceException = validTenantCache.getUnchecked(id);
if (serviceException != SERVICE_EXCEPTION_NULL) {
throw serviceException;
}
}
}

View File

@ -0,0 +1,93 @@
package cn.iocoder.yudao.framework.tenant.core.util;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import java.util.Map;
import java.util.concurrent.Callable;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* 多租户 Util
*
* @author 芋道源码
*/
public class TenantUtils {
/**
* 使用指定租户,执行对应的逻辑
*
* 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
* 当然,执行完成后,还是会恢复回去
*
* @param tenantId 租户编号
* @param runnable 逻辑
*/
public static void execute(Long tenantId, Runnable runnable) {
Long oldTenantId = TenantContextHolder.getTenantId();
Boolean oldIgnore = TenantContextHolder.isIgnore();
try {
TenantContextHolder.setTenantId(tenantId);
TenantContextHolder.setIgnore(false);
// 执行逻辑
runnable.run();
} finally {
TenantContextHolder.setTenantId(oldTenantId);
TenantContextHolder.setIgnore(oldIgnore);
}
}
/**
* 使用指定租户,执行对应的逻辑
*
* 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
* 当然,执行完成后,还是会恢复回去
*
* @param tenantId 租户编号
* @param callable 逻辑
*/
public static <V> V execute(Long tenantId, Callable<V> callable) {
Long oldTenantId = TenantContextHolder.getTenantId();
Boolean oldIgnore = TenantContextHolder.isIgnore();
try {
TenantContextHolder.setTenantId(tenantId);
TenantContextHolder.setIgnore(false);
// 执行逻辑
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
TenantContextHolder.setTenantId(oldTenantId);
TenantContextHolder.setIgnore(oldIgnore);
}
}
/**
* 忽略租户,执行对应的逻辑
*
* @param runnable 逻辑
*/
public static void executeIgnore(Runnable runnable) {
Boolean oldIgnore = TenantContextHolder.isIgnore();
try {
TenantContextHolder.setIgnore(true);
// 执行逻辑
runnable.run();
} finally {
TenantContextHolder.setIgnore(oldIgnore);
}
}
/**
* 将多租户编号,添加到 header 中
*
* @param headers HTTP 请求 headers
* @param tenantId 租户编号
*/
public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
if (tenantId != null) {
headers.put(HEADER_TENANT_ID, tenantId.toString());
}
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.tenant.core.web;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 多租户 Context Web 过滤器
* 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
*
* @author 芋道源码
*/
public class TenantContextWebFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 设置
Long tenantId = WebFrameworkUtils.getTenantId(request);
if (tenantId != null) {
TenantContextHolder.setTenantId(tenantId);
}
try {
chain.doFilter(request, response);
} finally {
// 清理
TenantContextHolder.clear();
}
}
}

View File

@ -0,0 +1,17 @@
/**
* 多租户,支持如下层面:
* 1. DB基于 MyBatis Plus 多租户的功能实现。
* 2. Redis通过在 Redis Key 上拼接租户编号的方式,进行隔离。
* 3. Web请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
* 4. Security校验当前登陆的用户是否越权访问其它租户的数据。
* 5. Job在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
* 6. MQ在 Producer 发送消息时Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
* 7. Async异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
* 1Spring Async
* {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
* 2Spring Security
* TransmittableThreadLocalSecurityContextHolderStrategy
* 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
*
*/
package cn.iocoder.yudao.framework.tenant;

View File

@ -0,0 +1,269 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.handler.invocation;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* Extension of {@link HandlerMethod} that invokes the underlying method with
* argument values resolved from the current HTTP request through a list of
* {@link HandlerMethodArgumentResolver}.
*
* 针对 rabbitmq-spring 和 kafka-spring不存在合适的拓展点可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中
* TODO 芋艿:持续跟进,看看有没新的拓展点
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 4.0
*/
public class InvocableHandlerMethod extends HandlerMethod {
private static final Object[] EMPTY_ARGS = new Object[0];
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* Create an instance from a {@code HandlerMethod}.
*/
public InvocableHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
/**
* Create an instance from a bean instance and a method.
*/
public InvocableHandlerMethod(Object bean, Method method) {
super(bean, method);
}
/**
* Construct a new handler method with the given bean instance, method name and parameters.
* @param bean the object bean
* @param methodName the method name
* @param parameterTypes the method parameter types
* @throws NoSuchMethodException when the method cannot be found
*/
public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
throws NoSuchMethodException {
super(bean, methodName, parameterTypes);
}
/**
* Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values.
*/
public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
this.resolvers = argumentResolvers;
}
/**
* Set the ParameterNameDiscoverer for resolving parameter names when needed
* (e.g. default request attribute name).
* <p>Default is a {@link DefaultParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* Invoke the method after resolving its argument values in the context of the given message.
* <p>Argument values are commonly resolved through
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
* The {@code providedArgs} parameter however may supply argument values to be used directly,
* i.e. without argument resolution.
* <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
* resolved arguments.
* @param message the current message being processed
* @param providedArgs "given" arguments matched by type, not resolved
* @return the raw value returned by the invoked method
* @throws Exception raised if no suitable argument resolver can be found,
* or if the method raised an exception
* @see #getMethodArgumentValues
* @see #doInvoke
*/
@Nullable
public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(message, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
// 注意:如下是本类的改动点!!!
// 情况一:无租户编号的情况
Long tenantId= parseTenantId(message);
if (tenantId == null) {
return doInvoke(args);
}
// 情况二:有租户的情况下
return TenantUtils.execute(tenantId, () -> doInvoke(args));
}
private Long parseTenantId(Message<?> message) {
Object tenantId = message.getHeaders().get(HEADER_TENANT_ID);
if (tenantId == null) {
return null;
}
if (tenantId instanceof Long) {
return (Long) tenantId;
}
if (tenantId instanceof Number) {
return ((Number) tenantId).longValue();
}
if (tenantId instanceof String) {
return Long.parseLong((String) tenantId);
}
if (tenantId instanceof byte[]) {
return Long.parseLong(new String((byte[]) tenantId));
}
throw new IllegalArgumentException("未知的数据类型:" + tenantId);
}
/**
* Get the method argument values for the current message, checking the provided
* argument values and falling back to the configured argument resolvers.
* <p>The resulting array will be passed into {@link #doInvoke}.
* @since 5.1.2
*/
protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new MethodArgumentResolutionException(
message, parameter, formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, message);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
/**
* Invoke the handler method with the given argument values.
*/
@Nullable
protected Object doInvoke(Object... args) throws Exception {
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}
MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
return new AsyncResultMethodParameter(returnValue);
}
private class AsyncResultMethodParameter extends HandlerMethodParameter {
@Nullable
private final Object returnValue;
private final ResolvableType returnType;
public AsyncResultMethodParameter(@Nullable Object returnValue) {
super(-1);
this.returnValue = returnValue;
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
}
protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
super(original);
this.returnValue = original.returnValue;
this.returnType = original.returnType;
}
@Override
public Class<?> getParameterType() {
if (this.returnValue != null) {
return this.returnValue.getClass();
}
if (!ResolvableType.NONE.equals(this.returnType)) {
return this.returnType.toClass();
}
return super.getParameterType();
}
@Override
public Type getGenericParameterType() {
return this.returnType.getType();
}
@Override
public AsyncResultMethodParameter clone() {
return new AsyncResultMethodParameter(this);
}
}
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
cn.iocoder.yudao.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor

View File

@ -0,0 +1 @@
cn.iocoder.yudao.framework.tenant.config.YudaoTenantAutoConfiguration