概述
Spring cloud 使用feign+mybatie-plus的dynamic-datasource来实现动态数据源切换
接 上一篇 手把手教你快速将DUBBO开发框架的系统SAAS化 文章,再来对一些springcloud的项目进行SAAS改造,
众所周知的是spring cloud基于springboot 做业务微服务,eurka做服务发现,zuul做网关,config做配置管理等组件构建了一套在7层上通讯的完整架构,各个微服务之间由feign(http协议的封装)+ribbon(负载均衡)来完成服务与服务之间的调用。
几个关键的技术点:
1 com.alibaba.ttl.TransmittableThreadLocal
2 com.baomidou.dynamic.datasource.DynamicRoutingDataSource
3 OkHttpClient,okhttp3.Interceptor
4 org.springframework.web.filter.OncePerRequestFilter
Step 1 在请求中携带租户标识
package cn.mwcare.framework.config.openfeign.okhttp;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.extern.java.Log;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.util.StringUtils;
/**
*
- @author arthur
*/
@Log
public class OkHttpLogInterceptor implements Interceptor {
private final String TENANTPREFIX = “tenantPrefix”;
private final String TENANTPREFIX_DEFAULT = “master”;
private static final String TX_XID = "TX_XID";
@Autowired
ApplicationContext context;
static TransmittableThreadLocal<String> contextparam = new TransmittableThreadLocal<>();
private static String tenantPrefixtx = null;
// 提供线程局部变量set方法
public static void setTenantPrefix(String tenantPrefix) {
tenantPrefixtx = tenantPrefix;
contextparam.copy(tenantPrefixtx);
contextparam.set(tenantPrefixtx);
}
// 提供线程局部变量get方法
public static String getTenantPrefix() {
return contextparam.get();
}
/**
* 清空 threadLocal 保存的对象
*
* @return
*/
public static void clear() {
contextparam.remove();
}
@Override
public Response intercept(Chain chain) throws IOException {
// 这个chain里面包含了request和response,所以你要什么都可以从这里拿
Request request = chain.request();
// 如果存在 tenantDomain 添加请求头
String tenantPrefix = tenantPrefixtx;
Request.Builder requestBuilder = request.newBuilder();
if (tenantPrefix != null) {
requestBuilder.addHeader(TENANTPREFIX, tenantPrefix.toString());
OkHttpLogInterceptor.clear();
} else {
requestBuilder.addHeader(TENANTPREFIX, TENANTPREFIX_DEFAULT);
OkHttpLogInterceptor.clear();
}
// for hystrix 线程池 模式 传递 XID
String tx_xid = request.header(TX_XID);
if (StringUtils.isEmpty(tx_xid)) {
if (TransmittableThreadLocalContext.get(TX_XID) != null) {
String xid = TransmittableThreadLocalContext.get(TX_XID).toString();
requestBuilder.addHeader(TX_XID, xid);
}
}
request = requestBuilder.build();
// 请求发起的时间
long t1 = System.nanoTime();
log.info(String.format("发送请求 %s on %s%n%s", request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
// 收到响应的时间
long t2 = System.nanoTime();
// 这里不能直接使用response.body().string()的方式输出日志
// 因为response.body().string()之后,response中的流会被关闭,程序会报错,我们需要创建出一
// 个新的response给应用层处理
ResponseBody responseBody = response.peekBody(1024 * 1024);
String jsonResponseBody = responseBody.string();
/*
* Result rs = JSON.parseObject(jsonResponseBody, Result.class); if (500 ==
* rs.getCode()) { throw new RenException(rs.getMsg(), rs.getCode()); }
*/
log.info(String.format("接收响应: [%s] %n返回json:【%s】 %.1fms%n%s", response.request().url(), jsonResponseBody,
(t2 - t1) / 1e6d, response.headers()));
return response;
}
}
也就是从 controller–> service 是使用feign完成的远程调用,这个时候 OkHttpLogInterceptor.setTenantPrefix(“System”);
在intercept 方法中 requestBuilder.addHeader(TENANTPREFIX, tenantPrefix.toString()); 这个时候feign调用的http请求头中完成了参数的上下文传递。
Step 2 在服务中加载多数据源
package cn.mwcare.framework.dynamicdatasources;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.alibaba.druid.wall.WallFilterMBean;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DruidDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.druid.DruidConfig;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.druid.DruidStatConfig;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.druid.DruidWallConfig;
import cn.mwcare.tenant.application.service.client.TenantServiceClient;
import cn.mwcare.tenant.domain.repository.entity.SysTenantAppDbEntity;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
/**
- 初始化该服务的数据源
- @author arthur
*/
@Component(“dynamicRoutingDataSourceConfig”)
@Slf4j
public class DynamicRoutingDataSourceConfig implements ApplicationRunner { // implements ApplicationListener {
@Value("${spring.application.name}")
private String APP_ID;
@Autowired(required = false)
TenantServiceClient tenantServiceClient;
@Autowired
private DruidDataSourceCreator druidDataSourceCreator;
@Autowired
DynamicRoutingDataSource ds;
@Override
public void run(ApplicationArguments args) {
// Thread.sleep(2000l);
DynamicRoutingDataSourceConfig.addCheckDS(APP_ID, null, tenantServiceClient, druidDataSourceCreator, ds);
}
public static boolean addCheckDS(String appId, String tenantPrifx, TenantServiceClient tenantServiceClient, DruidDataSourceCreator druidDataSourceCreator, DynamicRoutingDataSource ds) {
boolean ishavaDatasour = false;
try {
if (!StringUtils.isEmpty(tenantPrifx) && tenantPrifx.equals("master")) {
log.info("Ds {} is exits ,unnecessary check -------!!!", tenantPrifx);
return true;
}
List<SysTenantAppDbEntity> tenantConfigEntities = tenantServiceClient.findTenantMysqlDbData(appId);
for (SysTenantAppDbEntity entity : tenantConfigEntities) {
String beanKey = entity.getTenantPrefix();
if (!StringUtils.isEmpty(tenantPrifx) && tenantPrifx.equals(beanKey)) {
ishavaDatasour = true;
}
DataSourceVO dto = new DataSourceVO();
dto.setPoolName(beanKey);
dto.setDriverClassName(entity.getDbDriverClassName());
dto.setUrl(entity.getDbUrl());
dto.setUsername(entity.getDbUserName());
dto.setPassword(entity.getDbPassWord());
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(false);
dataSourceProperty.setDruid(DynamicRoutingDataSourceConfig.initDruidConfig());
DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(beanKey, dataSource);
System.out.println("add tenant app datasource : {" + beanKey + "},{" + appId + "} sucess!!!");
}
return ishavaDatasour;
} catch (Exception e) {
if (log.isErrorEnabled()) {
System.out.println("get dynamic other datasource is null pleas check APP : {" + appId + "} tenant datasources config!!!! reason: {" + e.getMessage() + "}");
}
log.warn("get dynamic other datasource is null pleas check APP : {} tenant datasources config!!!! reason: {}", appId, e.getMessage());
}
return ishavaDatasour;
}
public static DruidConfig initDruidConfig() {
DruidConfig dcf = new DruidConfig();
dcf.setInitialSize(20);
dcf.setMaxActive(1000);
dcf.setMinIdle(150);
dcf.setMaxWait(60000);
dcf.setPoolPreparedStatements(true);
dcf.setUseGlobalDataSourceStat(true);
dcf.setMaxPoolPreparedStatementPerConnectionSize(20);
dcf.setTimeBetweenEvictionRunsMillis(600000L);
dcf.setMinEvictableIdleTimeMillis(1800000L);
dcf.setInitConnectionSqls("SET NAMES utf8mb4");
dcf.setTestWhileIdle(true);
dcf.setTestOnBorrow(true);
dcf.setTestOnReturn(false);
dcf.setRemoveAbandoned(true);
dcf.setRemoveAbandonedTimeoutMillis(120);
dcf.setLogAbandoned(true);
DruidStatConfig dsc = new DruidStatConfig();
dsc.setLogSlowSql(true);
dsc.setMergeSql(false);
dsc.setSlowSqlMillis(3000L);
dcf.setStat(dsc);
DruidWallConfig wallconfig = new DruidWallConfig();
wallconfig.setMultiStatementAllow(true);
dcf.setWall(wallconfig);
return dcf;
}
}
implements ApplicationRunner 表示服务启动后执行 run 方法,
TenantServiceClient tenantServiceClient; 这个接口的服务要优先启动,
整个过程如就是启动服务,从接口获取该服务分配了哪些数据源
ds.addDataSource(beanKey, dataSource);
将数据源加载到ioc容器中
Step3 依据携带的租户标识进行数据源的选择
package cn.mwcare.framework.dynamicdatasources;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DruidDataSourceCreator;
import cn.mwcare.framework.config.openfeign.okhttp.OkHttpLogInterceptor;
import cn.mwcare.tenant.application.service.client.TenantServiceClient;
import lombok.extern.slf4j.Slf4j;
/**
- 效验检查请求中的租户标识
- @author arthur
*/
@Slf4j
public class CheckDSFilter extends OncePerRequestFilter {
private String APP_ID;
private TenantServiceClient tenantServiceClient;
private DruidDataSourceCreator druidDataSourceCreator;
private ApplicationContext context;
private final String TENANTPREFIX = "tenantPrefix";
private final String TENANTPREFIX_DEFAULT = "master";
public CheckDSFilter(String APP_ID, TenantServiceClient tenantServiceClient, DruidDataSourceCreator druidDataSourceCreator, ApplicationContext context) {
this.APP_ID = APP_ID;
this.tenantServiceClient = tenantServiceClient;
this.druidDataSourceCreator = druidDataSourceCreator;
this.context = context;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tenantPrefix = request.getHeader(TENANTPREFIX);
if (APP_ID.contains("boss")) {
OkHttpLogInterceptor.setTenantPrefix(TENANTPREFIX_DEFAULT);
filterChain.doFilter(request, response);
return;
}
if (StringUtils.isEmpty(tenantPrefix)) {
filterChain.doFilter(request, response);
return;
}
// log.info(“CheckDSFilter -----------------APP_ID:{}----------- tenantPrefix : {}”, APP_ID, tenantPrefix);
// if (log.isErrorEnabled()) {
// System.out.println((“CheckDSFilter -----------------APP_ID:{” + APP_ID + “}----------- tenantPrefix : {” + tenantPrefix + “}”));
// }
try {
DynamicRoutingDataSource ds = context.getBean(DynamicRoutingDataSource.class);
if (tenantPrefix != null && !tenantPrefix.equals(TENANTPREFIX_DEFAULT) && ds.getCurrentDataSources().get(tenantPrefix) == null) {
boolean ishavaDs = DynamicRoutingDataSourceConfig.addCheckDS(APP_ID, tenantPrefix, tenantServiceClient, druidDataSourceCreator, ds);
if (!ishavaDs) {
tenantPrefix = TENANTPREFIX_DEFAULT;
}
}
} catch (Exception e) {
if (log.isErrorEnabled() || !log.isInfoEnabled()) {
System.out.println("!!! check datasource fail ,tenantPrefix is {" + tenantPrefix + "} “);
}
log.warn(”!!! check datasource fail ,tenantPrefix is {} ", tenantPrefix);
}
OkHttpLogInterceptor.setTenantPrefix(tenantPrefix);
filterChain.doFilter(request, response);
}
}
这是一个拦截器针对feign接口的调用校验header中是否存在数据源的key
以上几步完成了以后确保 header存在key,ds中存在key对应的datasources
在 feign接口实现类上或者方法上增加注解
@DS("#header.tenantPrefix")
完成!
最后
以上就是风中毛巾为你收集整理的手把手教你快速将Spring cloud开发框架的系统SAAS化的全部内容,希望文章能够帮你解决手把手教你快速将Spring cloud开发框架的系统SAAS化所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复