package cn.iocoder.yudao.framework.tenant.config; import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi; 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.security.core.service.SecurityFrameworkService; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 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.tenant.core.web.TenantVisitContextInterceptor; import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import jakarta.annotation.Resource; 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.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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 org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.pattern.PathPattern; import java.util.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @AutoConfiguration @ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户 @EnableConfigurationProperties(TenantProperties.class) public class YudaoTenantAutoConfiguration { @Resource private ApplicationContext applicationContext; @Bean public TenantFrameworkService tenantFrameworkService(TenantCommonApi 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() { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new TenantContextWebFilter()); registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); return registrationBean; } @Bean public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties, SecurityFrameworkService securityFrameworkService) { return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService); } @Bean public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties, TenantVisitContextInterceptor tenantVisitContextInterceptor) { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tenantVisitContextInterceptor) .excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0])); } }; } // ========== Security ========== @Bean public FilterRegistrationBean tenantSecurityWebFilter(TenantProperties tenantProperties, WebProperties webProperties, GlobalExceptionHandler globalExceptionHandler, TenantFrameworkService tenantFrameworkService) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new TenantSecurityWebFilter(webProperties, tenantProperties, getTenantIgnoreUrls(), globalExceptionHandler, tenantFrameworkService)); registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); return registrationBean; } /** * 如果 Controller 接口上,有 {@link TenantIgnore} 注解,则添加到忽略租户的 URL 集合中 * * @return 忽略租户的 URL 集合 */ @SuppressWarnings("removal") private Set getTenantIgnoreUrls() { Set ignoreUrls = new HashSet<>(); // 获得接口对应的 HandlerMethod 集合 RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping"); Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); // 获得有 @TenantIgnore 注解的接口 for (Map.Entry entry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); if (!handlerMethod.hasMethodAnnotation(TenantIgnore.class) // 方法级 && !handlerMethod.getBeanType().isAnnotationPresent(TenantIgnore.class)) { // 接口级 continue; } // 添加到忽略的 URL 中 if (entry.getKey().getPatternsCondition() != null) { ignoreUrls.addAll(entry.getKey().getPatternsCondition().getPatterns()); } if (entry.getKey().getPathPatternsCondition() != null) { ignoreUrls.addAll( convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString)); } } return ignoreUrls; } // ========== MQ ========== @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor") public static class TenantRedisMQConfiguration { @Bean public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { return new TenantRedisMessageInterceptor(); } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") public static class TenantRabbitMQConfiguration { @Bean public TenantRabbitMQInitializer tenantRabbitMQInitializer() { return new TenantRabbitMQInitializer(); } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") public static class TenantRocketMQConfiguration { @Bean public TenantRocketMQInitializer tenantRocketMQInitializer() { return new TenantRocketMQInitializer(); } } // ========== Redis ========== @Bean @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean public RedisCacheManager tenantRedisCacheManager(RedisTemplate redisTemplate, RedisCacheConfiguration redisCacheConfiguration, YudaoCacheProperties yudaoCacheProperties, TenantProperties tenantProperties) { // 创建 RedisCacheWriter 对象 RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); // 创建 TenantRedisCacheManager 对象 TenantRedisCacheManager cacheManager = new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches()); // 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit, // 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变 cacheManager.setTransactionAware(true); return cacheManager; } // ========== Job ========== @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "cn.iocoder.yudao.framework.quartz.core.handler.JobHandler") public static class TenantJobConfiguration { @Bean public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { return new TenantJobAspect(tenantFrameworkService); } } }