基于Ruoyi_vue多租户的实现
基于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