手把手教你快速将Spring cloud开发框架的系统SAAS化

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")

完成!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值