基于Ruoyi_vue多租户的实现

如何根据租户信息切换数据源的

我们看到 TenantInterceptor 这个类

@Component
@Slf4j
public class TenantInterceptor implements HandlerInterceptor {

    @Autowired
    private IMasterTenantService masterTenantService;

    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;

    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String url = request.getServletPath();
        String tenant= request.getHeader("tenant");
        log.info("&&&&&&&&&&&&&&&& 租户拦截 &&&&&&&&&&&&&&&&");
        if (StringUtils.isNotBlank(tenant)) {
            if (!dynamicRoutingDataSource.existDataSource(tenant)) {
                //搜索默认数据库,去注册租户的数据源,下次进来直接session匹配数据源
                MasterTenant masterTenant = masterTenantService.selectMasterTenant(tenant);
                if (masterTenant == null) {
                    throw new RuntimeException("无此租户:"+tenant );
                }else if(TenantStatus.DISABLE.getCode().equals(masterTenant.getStatus())){
                    throw new RuntimeException("租户["+tenant+"]已停用" );
                }else if(masterTenant.getExpirationDate()!=null){
                    if(masterTenant.getExpirationDate().before(DateUtils.getNowDate())){
                        throw new RuntimeException("租户["+tenant+"]已过期");
                    }
                }
                Map<String, Object> map = new HashMap<>();
                map.put("driverClassName", driverClassName);
                map.put("url", masterTenant.getUrl());
                map.put("username", masterTenant.getUsername());
                map.put("password", masterTenant.getPassword());
                dynamicRoutingDataSource.addDataSource(tenant, map);

                log.info("&&&&&&&&&&& 已设置租户:{} 连接信息: {}", tenant, masterTenant);
            }else{
                log.info("&&&&&&&&&&& 当前租户:{}", tenant);
            }
        }else{
            throw new RuntimeException("缺少租户信息");
        }
        // 为了单次请求,多次连接数据库的情况,这里设置localThread,AbstractRoutingDataSource的方法去获取设置数据源
        DynamicDataSourceContextHolder.setDataSourceKey(tenant);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 请求结束删除localThread
        DynamicDataSourceContextHolder.clearDataSourceKey();
    }
}

在 ResourcesConfig 里面注册了这个拦截器

@Override
public void addInterceptors(InterceptorRegistry registry)
{

// ....

    registry.addInterceptor(tenantInterceptor).addPathPatterns("/**").excludePathPatterns("/captchaImage").excludePathPatterns("/register");
}

可以看到,排除了注册和验证码接口,这两个接口显然是不需要切换租户数据源的

具体看下拦截器实现

所有 header 会带上 tenant ,解析出 租户名

从 主数据源拿到租户数据源配置信息

@Override
@DataSource(DataSourceType.MASTER)
public MasterTenant selectMasterTenant(String tenant) {

MasterTenant masterTenant = new MasterTenant();
masterTenant.setTenant(tenant);
return masterTenantMapper.selectMasterTenant(masterTenant);

}

这块注意一点要手动指定数据源为 master,后面一小节会简单说下 若依的多数据源实现,以及和这里的多租户插件是否存在冲突的分析

给当前线程绑定数据源 DynamicDataSourceContextHolder.setDataSourceKey(tenant);

DynamicDataSourceContextHolder 实现就是创建了 ThreadLocal,用于绑定数据源信息

/**

  • 数据源切换处理
    *
  • @author devjd
    */

@Slf4j
public class DynamicDataSourceContextHolder {

private static final ThreadLocal<String> db = new ThreadLocal<>();

public static void setDataSourceKey(String key) {
    db.set(key);
}

public static String getDataSourceKey() {
    return db.get();
}

public static void clearDataSourceKey() {
    db.remove();
}

}

请求结束清理 DynamicDataSourceContextHolder

若依多数据源的实现

标签: none

添加新评论