springboot项目实现分库
本文是根据仓库编码 和 仓库id进行按仓库进行分库处理,可以根据例子自行按照业务需要进行分库
1.核心是实现 Spring 的 AbstractRoutingDataSource 抽象类,重写 determineCurrentLookupKey 方法,实现动态数据源的目的
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* ThreadLocal 线程独立存储
*/
private static final ThreadLocal<String> THREAD_HOLD_SOURCE = new InheritableThreadLocal<>();
/**
* 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
*
* @param defaultTargetDataSource 默认数据源
* @param targetDataSources 目标数据源
*/
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
/**
* 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
* 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
*/
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
/**
* 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
/**
* 通过同一线程获取对应
*/
public static String getDataSource() {
log.info("获取选择对应的数据源名称:{}", THREAD_HOLD_SOURCE.get());
return THREAD_HOLD_SOURCE.get();
}
public static void setDataSource(String sourceName) {
log.info("设置对应的数据源名称:{}", sourceName);
THREAD_HOLD_SOURCE.set(sourceName);
}
public static void clearDataSource() {
THREAD_HOLD_SOURCE.remove();
}
}
2.springboot项目启动类同级配置动态数据源配置
@Configuration
public class DynamicDataSourceConfig {
/**
* 数据源01 默认数据源
*/
@Bean(name = DataSourceConst.test_01)
@ConfigurationProperties("spring.datasource.test01")
public DataSource test01() {
return DruidDataSourceBuilder.create().build();
}
/**
* 数据源02
*/
@Bean(name = DataSourceConst.test_02)
@ConfigurationProperties("spring.datasource.test02")
public DataSource test02() {
return DruidDataSourceBuilder.create().build();
}
/**
* 从库数据源
*/
@Bean(name = DataSourceConst.test_01_SLAVE)
@ConfigurationProperties("spring.datasource.test01-slave")
public DataSource test01Slave() {
return DruidDataSourceBuilder.create().build();
}
/**
* 数据源02
*/
@Bean(name = DataSourceConst.test_02_SLAVE)
@ConfigurationProperties("spring.datasource.test02-slave")
public DataSource test02Slave() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource(@Qualifier("test01") DataSource test01, @Qualifier("test02") DataSource test02,
@Qualifier("test01-slave") DataSource test01Slave, @Qualifier("test02-slave") DataSource test02Slave) {
//构建数据源集合
Map<Object, Object> dataSourceMap = new HashMap<>(5);
dataSourceMap.put(DataSourceConst.test_01, test01);
dataSourceMap.put(DataSourceConst.test_02, test02);
dataSourceMap.put(DataSourceConst.test_01_SLAVE, test01Slave);
dataSourceMap.put(DataSourceConst.test_02_SLAVE, test02Slave);
return new DynamicDataSource(test01, dataSourceMap);
}
@Bean(name = "tidbJdbcTemplate")
public JdbcTemplate tidbJdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
pom文件引入包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
3.在公共模块配置所需配置Bean
public class DataSourceConst {
public static final String test_01 = "test01";
public static final String test_02 = "test02";
public static final String test_01_SLAVE = "test01-slave";
public static final String test_02_SLAVE = "test02-slave";
/获取仓库编码字段//
public static final String WH_CODE = "whCode";
///获取动态数据源redisKey///
public static final String DATA_SOURCE_REDIS_KEY = "dynamic:data:source:";
public static final String DATA_SOURCE_PROJECT_REDIS_KEY = "dynamic:project:data:source:";
}
4.创建自定义异常类及枚举值
/**
* 异常错误code
* 重复操作
*/
@AllArgsConstructor
@Getter
public enum ErrorCodeEnum {
VALID_ERROR_MESSAGE("10001", "参数校验异常"),
ERROR_PARAMS("10002", "参数异常");
private String code;
private String message;
}
@Setter
@Getter
public class CustomException extends RuntimeException{
private String code;
private String message;
public CustomException(String message) {
this.message = message;
}
public CustomException(ErrorCodeEnum errorCodeEnum) {
this.code = errorCodeEnum.getCode();
this.message = errorCodeEnum.getMessage();
}
public CustomException(String message, ErrorCodeEnum errorCodeEnum) {
this.code = errorCodeEnum.getCode();
this.message = message;
}
public CustomException(String code, String message) {
this.code = code;
this.message = message;
}
}
5.在全局异常处理类中配置全局公共返回
@Slf4j
@ControllerAdvice
public class TestServerGlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
@ResponseBody
public String handleCustomException(CustomException e) {
log.error("自定义异常:{},{}", e.getMessage(), e);
String result ="操作失败:"+e.getMessage();
return result;
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleException(Exception e) {
log.error("接口异常:{},{}", e.getMessage(), e);
String result ="操作失败:"+e.getMessage();
return result ;
}
}
6.编写获取动态数据源公共类,动态
@Slf4j
@Component
public class DataSourceUtil {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TestDataMappingManager testDataMappingManager;
/**
* 通过映射表 仓库编码 获取对应数据源的映射关系
*/
public String dataSourceByWhCode(String whCode) {
Assert.notNull(whCode, "仓库编码不能为空");
String dataSourceName = null;
final String WHCODE_SOURCE_REDIS_KEY = DataSourceConst.DATA_SOURCE_REDIS_KEY + whCode;
//判断缓存是否命中
if (redisTemplate.hasKey(WHCODE_SOURCE_REDIS_KEY)) {
dataSourceName = String.valueOf(redisTemplate.opsForValue().get(WHCODE_SOURCE_REDIS_KEY));
} else {
//查询数据库
TestDataMappingModel TestDataMappingModel = new TestDataMappingModel();
TestDataMappingModel.setWhCode(whCode);
List<TestDataMappingModel> byExample = testDataMappingManager.findByExample(TestDataMappingModel);
//这里如果返回多条或者为空 则不做处理抛出异常
if (byExample != null && byExample.size() == 1) {
TestDataMappingModel TestDataMappingModelData = byExample.get(0);
redisTemplate.opsForValue().set(WHCODE_SOURCE_REDIS_KEY, TestDataMappingModelData.getDataSourceName());
dataSourceName = TestDataMappingModelData.getDataSourceName();
}
}
return dataSourceName;
}
/**
* 通过映射表 仓库ID 获取对应数据源的映射关系
*/
public String dataSourceByWhId(String projectId) {
Assert.notNull(projectId, "仓库ID不能为空");
String dataSourceName = null;
final String DATA_SOURCE_PROJECT_REDIS_KEY = DataSourceConst.DATA_SOURCE_PROJECT_REDIS_KEY + projectId;
//判断缓存是否命中
if (redisTemplate.hasKey(DATA_SOURCE_PROJECT_REDIS_KEY)) {
dataSourceName = String.valueOf(redisTemplate.opsForValue().get(DATA_SOURCE_PROJECT_REDIS_KEY));
} else {
//查询数据库
TestDataMappingModel TestDataMappingModel = new TestDataMappingModel();
TestDataMappingModel.setWhId(Long.valueOf(projectId));
List<TestDataMappingModel> byExample = testDataMappingManager.findByExample(TestDataMappingModel);
//这里如果返回多条或者为空 则不做处理抛出异常
if (byExample != null && byExample.size() == 1) {
TestDataMappingModel TestDataMappingModelData = byExample.get(0);
redisTemplate.opsForValue().set(DATA_SOURCE_PROJECT_REDIS_KEY, TestDataMappingModelData.getDataSourceName());
dataSourceName = TestDataMappingModelData.getDataSourceName();
}
}
return dataSourceName;
}
/**
* 通过注解信息获应仓库编码字段内容映射获取数据源 以及 selectName选项
* 注意 这里仅支持获取第一个参数值的 并且仅支持第一个对象参数第一层,不支持嵌套
* <p>
* 这里注明如果选择selectName 则优先获取对应数据源
*
* @return
*/
public String getwhCodeByAnnotation(JoinPoint proceedingJoinPoint, DSource dSource) {
String dataSourceName = null;
try {
String whCode = null;
String projectId = null;
if(dSource !=null){
//通过注解selectName选项获取
if (dSource.selectName().length() != 0) {
return dSource.selectName();
}
//通过jsonPath表达式来解析对应的仓库编码字段
if (StringUtils.isNotBlank(dSource.jsonPathExpression())) {
String jsonBody = JSON.toJSONString(proceedingJoinPoint.getArgs()[0]);
if (StringUtils.isNotEmpty(jsonBody) && StringUtils.isNotBlank(jsonBody)) {
whCode = JsonPath.parse(jsonBody).read(dSource.jsonPathExpression());
}
}
//通过jsonPath表达式来解析对应的projectId字段
if (StringUtils.isNotBlank(dSource.jsonPathExpression2())) {
String jsonBody = JSON.toJSONString(proceedingJoinPoint.getArgs()[0]);
if (StringUtils.isNotEmpty(jsonBody) && StringUtils.isNotBlank(jsonBody)) {
projectId = JsonPath.parse(jsonBody).read(dSource.jsonPathExpression2());
dataSourceName = dataSourceByWhId(projectId);
}
}
//通过参数值来获取对应的仓库Id 这里支持获取第一参数的内容
if (dSource.useUserUtilBean()) {
if (proceedingJoinPoint.getArgs()[0] instanceof String) {
JSONObject jobParamsObject = JSONObject.parseObject(String.valueOf(proceedingJoinPoint.getArgs()[0]));
UserUtilBean userUtilBean = JSONObject.toJavaObject(jobParamsObject, UserUtilBean.class);
projectId = userUtilBean.getProjectId();
log.info("获取定时任务仓库id:{}", projectId);
dataSourceName = dataSourceByWhId(projectId);
}else if(proceedingJoinPoint.getArgs()[0] instanceof UserUtilBean ){
UserUtilBean userUtilBean = (UserUtilBean) proceedingJoinPoint.getArgs()[0];
projectId = userUtilBean.getProjectId();
log.info("获取定时任务仓库id:{}", projectId);
dataSourceName = dataSourceByWhId(projectId);
}
}
Object[] args = proceedingJoinPoint.getArgs();
if (dataSourceName == null) {
for (Object o : args) {
if (o instanceof UserUtilBean) {
UserUtilBean userUtilBean = (UserUtilBean) o;
projectId = userUtilBean.getProjectId();
whCode = userUtilBean.getWhCode();
log.info("获取接口多个参数情况:{}", JSON.toJSONString(userUtilBean));
if(projectId!=null && !"".equals(projectId)){
log.info("获取接口多个参数情况的仓库id:{}", projectId);
dataSourceName = dataSourceByWhId(projectId);
}
if(whCode!=null && !"".equals(whCode) && dataSourceName == null ){
log.info("获取接口多个参数情况的仓库code:{}", whCode);
dataSourceName = dataSourceByWhCode(whCode);
}
}
}
}
}
if (StringUtils.isNotEmpty(whCode) && StringUtils.isNotBlank(whCode)) {
dataSourceName = dataSourceByWhCode(whCode);
}
} catch (Exception e) {
log.error("DataSourceAspect 获取仓库编码异常:{}", e);
}
if (StringUtils.isNotEmpty(dataSourceName) && StringUtils.isNotBlank(dataSourceName)) {
DynamicDataSource.setDataSource(dataSourceName);
} else {
throw new CustomException("数据源获取不能为空,请联系管理员");
}
return dataSourceName;
}
}
7.设置注解切点
/**
外部接口,内部项目接口对接使用注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DSource {
/**
* 这里用来处理根据注解字段来获取对应分片字段的值
*
* @return
*/
String columnName() default DataSourceConst.WH_CODE;
/**
* 是否通过userUtilBean projectId来获取
*
* @return
*/
boolean useUserUtilBean() default false;
/**
* 之类采用jsonPath表达式路径来获取对应的属性值
* 参考文档
* https://github.com/json-path/JsonPath
*
*/
String jsonPathExpression() default "" ;
/**
* 之类采用jsonPath表达式路径来获取对应的属性值 解析projectId
* 参考文档
* https://github.com/json-path/JsonPath
*
*/
String jsonPathExpression2() default "" ;
/**
* 手动选择使用那个逻辑数据源名称
*
* @return
*/
String selectName() default "";
}
/**
报表查询走只读 注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreSource {
}
8.编写web请求拦截器及反射,从而实现动态数据源
@Configuration
public class DataSourceFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
/* if(requestAttributes!=null){
HttpServletRequest requestServlet = requestAttributes.getRequest();
Enumeration<String> headerNames = requestServlet.getHeaderNames();
if(headerNames!=null){
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
String headerValue = requestServlet.getHeader(headerName);
// 将header向下传递
template.header(headerName,headerValue);
}
}
}*/
//获取对应请求头whCode参数字段内容
if(requestAttributes!=null){
if(requestAttributes.getRequest()!=null){
String whCode = requestAttributes.getRequest().getHeader("whCode");
if (whCode != null && !"".equals(whCode)) {
template.header("whCode", whCode);
}
}
}
}
}
@Order(2)
@Slf4j
@Aspect
@Component
public class OperatorRequestAspect {
@Autowired
private DataSourceUtil dataSourceUtil;
@PostConstruct
public void init() {
log.info("OperatorRequestAspect切面初始化");
}
@Pointcut("(@target(org.springframework.web.bind.annotation.RestController)) && (execution( * *..*.*ServiceImpl.*(..))) && !(@annotation(com.xx.dynamic.datasource.IgnoreSource)) && !(@annotation(com.xx.dynamic.datasource.DSource))")
//@Pointcut("(@target(org.springframework.web.bind.annotation.RestController)) && !(@annotation(com.xx.dynamic.datasource.DSource))")
public void feignControllerAspect() {
}
/**
* 方法调用之前调用
*/
@Around("feignControllerAspect()")
public Object doFeignAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object proceedResult = null;
try {
//判断当前方法是否有数据源
String dataSource = DynamicDataSource.getDataSource();
if (dataSource != null) {
DynamicDataSource.setDataSource(dataSource);
return proceedingJoinPoint.proceed();
}
String dataSourceName = null;
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
DSource ds = method.getAnnotation(DSource.class);
log.info("获取切面方法名称:{},切面类名称:{}", method.getName(), proceedingJoinPoint.getTarget().getClass());
//这里用来过滤DSource 注解切面逻辑 避免和其他切面处理业务重复
if (ds == null) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
//获取对应请求头whCode参数字段内容
String whCode = request.getHeader("whCode");
String projectId = request.getHeader("projectId");
//通过template获取对应whCode/projectId
Object[] args = proceedingJoinPoint.getArgs();
if ((whCode == null || "".equals(whCode)) && (projectId == null || "".equals(projectId))) {
for (Object o : args) {
if (o instanceof UserUtilBean) {
UserUtilBean userUtilBean = (UserUtilBean) o;
log.info("获取多参数userUtilBean:{}", JSON.toJSONString(userUtilBean));
projectId = userUtilBean.getProjectId();
whCode = userUtilBean.getWhCode();
}
}
}
if (projectId != null && !"".equals(projectId)) {
//执行projectId 映射获取对应数据源信息
dataSourceName = dataSourceUtil.dataSourceByWhId(projectId);
if (StringUtils.isNotEmpty(dataSourceName) && StringUtils.isNotBlank(dataSourceName)) {
DynamicDataSource.setDataSource(dataSourceName);
proceedResult = proceedingJoinPoint.proceed();
return proceedResult;
}
}
if (whCode != null && !"".equals(whCode)) {
//执行whCode 映射获取对应数据源信息
dataSourceName = dataSourceUtil.dataSourceByWhCode(whCode);
if (StringUtils.isNotEmpty(dataSourceName) && StringUtils.isNotBlank(dataSourceName)) {
DynamicDataSource.setDataSource(dataSourceName);
proceedResult = proceedingJoinPoint.proceed();
return proceedResult;
}
}
}
} finally {
DynamicDataSource.clearDataSource();
}
throw new CustomException("数据源获取不能为空,请联系管理员");
}
}
8.编写报表查询请求,走只读库反射
@Order(5)
@Slf4j
@Aspect
@Component
public class SlaveRequestAspect {
@Autowired
private DataSourceUtil dataSourceUtil;
private static final String SLAVE_PRIX = "-slave";
@PostConstruct
public void init() {
log.info("SlaveRequestAspect切面初始化");
}
@Pointcut("(@target(org.springframework.web.bind.annotation.RestController)) && (execution( * *..*.*ReportServiceImpl.*(..))) && (@annotation(com.xx.dynamic.datasource.IgnoreSource))")
public void reportControllerAspect() {
}
/**
* 方法调用之前调用
*/
@Around("reportControllerAspect()")
public Object doReportAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object proceedResult = null;
try {
//判断当前方法是否有数据源
String dataSource = DynamicDataSource.getDataSource();
String dataSourceName = null;
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
if (dataSource != null) {
DynamicDataSource.setDataSource(dataSource);
return proceedingJoinPoint.proceed();
}
log.info("获取切面方法名称:{},切面类名称:{}", method.getName(), proceedingJoinPoint.getTarget().getClass());
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
//获取对应请求头whCode参数字段内容
String whCode = request.getHeader("whCode");
String projectId = request.getHeader("projectId");
//通过template获取对应whCode/projectId
Object[] args = proceedingJoinPoint.getArgs();
if ((whCode == null || "".equals(whCode)) && (projectId == null || "".equals(projectId))) {
for (Object o : args) {
if (o instanceof UserUtilBean) {
UserUtilBean userUtilBean = (UserUtilBean) o;
log.info("获取多参数userUtilBean:{}", JSON.toJSONString(userUtilBean));
projectId = userUtilBean.getProjectId();
whCode = userUtilBean.getWhCode();
}
}
}
if (projectId != null && !"".equals(projectId)) {
//执行projectId 映射获取对应数据源信息
dataSourceName = dataSourceUtil.dataSourceByWhId(projectId);
if (StringUtils.isNotEmpty(dataSourceName) && StringUtils.isNotBlank(dataSourceName)) {
DynamicDataSource.setDataSource(dataSourceName + SLAVE_PRIX);
proceedResult = proceedingJoinPoint.proceed();
return proceedResult;
}
}
if (whCode != null && !"".equals(whCode)) {
//执行whCode 映射获取对应数据源信息
dataSourceName = dataSourceUtil.dataSourceByWhCode(whCode);
if (StringUtils.isNotEmpty(dataSourceName) && StringUtils.isNotBlank(dataSourceName)) {
DynamicDataSource.setDataSource(dataSourceName + SLAVE_PRIX);
proceedResult = proceedingJoinPoint.proceed();
return proceedResult;
}
}
} finally {
DynamicDataSource.clearDataSource();
}
throw new CustomException("数据源获取不能为空,请联系管理员");
}
}
9.编写注解反射,根据注解从而使接口请求,获取数据源
@Order(3)
@Aspect
@Slf4j
@Component
public class DataSourceAspect {
@Autowired
private DataSourceUtil dataSourceUtil;
@PostConstruct
public void init() {
log.info("DataSourceAspect切面初始化 ");
}
/**
* 定义pointCut
*/
@Pointcut("@annotation(com.sinoservices.dynamic.datasource.DSource) ")
public void dataSourcePoint() {
}
/**
* 默认数据源走wms01
* 其他情况走动态选择
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("dataSourcePoint()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//这通过切面获取对应分片字段值(仓库编码字段)
try {
//判断当前方法是否有数据源
String dataSource = DynamicDataSource.getDataSource();
if (dataSource != null) {
DynamicDataSource.setDataSource(dataSource);
return proceedingJoinPoint.proceed();
}
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
log.info("获取切面方法名称:{},切面类名称:{}", method.getName(), proceedingJoinPoint.getTarget().getClass());
DSource ds = method.getAnnotation(DSource.class);
// Object[] args = proceedingJoinPoint.getArgs();
// if (ds == null) {
// DynamicDataSource.setDataSource(DataSourceConst.WMS_01);
// log.debug("set default datasource is :{} ", DataSourceConst.WMS_01);
// } else {
//如果注解不会空,支持两种情况,
// 第一种 通过注解属性seletName选择对应数据源
// 第二种 通过注解属性columnName标注获取仓库编码字段名称来获取对应的仓库编码内容
String dataSourceName = dataSourceUtil.getwhCodeByAnnotation(proceedingJoinPoint, ds);
DynamicDataSource.setDataSource(dataSourceName);
// }
return proceedingJoinPoint.proceed();
} finally {
log.info("移除数据源:{}", DynamicDataSource.getDataSource());
DynamicDataSource.clearDataSource();
}
}
}
11.FeignInterceptor拦截器增加仓库编码参数
@Configuration
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
if("/loginService/login".equals(template.url())
|| "/loginService/loadUserDetails".equals(template.url())){
return;
}
UserDetails userDetails = UserContextHolder.getCurrentUser();
String language= "zh_CN";
if(null != userDetails && userDetails.getLanguage()!= null){
language = userDetails.getLanguage();
}
language=language.replace("_","-");
LocaleContextHolder.setLocale(Locale.forLanguageTag(language));
//添加仓库编码到对应请求头
if(ObjectUtils.isNotEmpty(userDetails)){
template.header("whCode",userDetails.getWhCode());
template.header("language",language);
}
}
}
10.注解使用案例
//test中包含 projectId 字段值
@DSource(jsonPathExpression2 = "$.projectId")
public String saveList(TestModel test) {
}
//直接包含用户对象
@DSource(useUserUtilBean = true)
public void testSave(UserUtilBean userUtilBean) {
}
//test中包含 whCode字段值
@DSource(jsonPathExpression = "$.whCode")
public String saveList(TestModel test) {
}
//直接设置数据源名称
@DSource(selectName = DataSourceConst.WMS_02_SLAVE)
public String saveList(TestModel test) {
}