注:數據源切換不可在事務內部、同一事務中數據源確定、不可切換!
亦可說數據源切換必須在事務處理之前!
一、功能介紹
在實際的開發中,同一個項目中使用多個數據源是很常見的場景。最近在項目中正好使用到多數據源、搭建完成後記錄一下!可供參考。
二、配置文件
application.yml中添加多數據源配置
spring:
application:
name: businesscooperation-server-dev
main:
allow-bean-definition-overriding: true
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
type: com.alibaba.druid.pool.DruidDataSource
# 多數據源 配置
datasource:
defaultName: master
driverClassName: com.mysql.cj.jdbc.Driver
# 多數據源配置數據源名稱
multipleDataSources: master,supplier
master:
url: jdbc:mysql://ip:3306/mboms_mall?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
supplier:
url: jdbc:mysql://ip:3306/mbproduct_mall?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
三、java處理
1、自定義數據源配置DBProperties.java
package com.hbis.mysql.config;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 自定義數據源配置
*
* @author henry
* @date 2020/6/19 14:34
*/
@Data
@Component
public class DBProperties {
@Resource
private Environment env;
@Value("#{'${datasource.multipleDataSources}'.split(',')}")
private List<String> multipleDataSources;
@Value("${datasource.driverClassName}")
private String driverClassName;
@Value("${datasource.defaultName}")
private String defaultName;
/**
* 數據源存儲
*/
private Map<Object, Object> dataSources = new HashMap<>();
private DruidDataSource initDataSource(String url, String username, String password) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
/**
* 初始化自定義數據源集
*/
void init() {
multipleDataSources.forEach(r -> dataSources.put(r, initDataSource(env.getProperty("datasource." + r + ".url"),
env.getProperty("datasource." + r + ".username"),
env.getProperty("datasource." + r + ".password"))));
}
/**
* 獲得默認的數據源 Get the default data source
*
* @return DruidDataSource
*/
DruidDataSource getDefaultDataSource() {
return (DruidDataSource) this.dataSources.get(defaultName);
}
}
2、數據源配置,將自定義的所有的數據源傳給數據源路由器
package com.hbis.mysql.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* 數據源配置,將自定義的所有的數據源傳給數據源路由器
*
* @author henry
* @date 2020/6/19 14:33
*/
@Configuration
public class DataSourceConfig {
@Resource
private DBProperties dbProperties;
/**
* 配置數據源 Configure data sources
*
* @return DataSource
*/
@Bean(name = "dataSource")
public DataSource dataSource() {
//初始化自定義數據源集
dbProperties.init();
// 採用AbstractRoutingDataSource的對象包裝多數據源
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(dbProperties.getDataSources());
// 設置默認的數據源,當拿不到指定數據源或者未指定數據源時使用該配置
dataSource.setDefaultTargetDataSource(dbProperties.getDefaultDataSource());
return dataSource;
}
/**
* 配置事務管理 Configuration transaction management
*
* @return PlatformTransactionManager
*/
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource());
}
}
3、動態數據源
package com.hbis.mysql.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 動態數據源 Dynamic data source
*
* @author henry
* @date 2020/6/19 14:36
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
//數據源路由器,獲取最終被執行的數據源
@Override
protected Object determineCurrentLookupKey() {
//從本地線程中獲取最終被執行的數據源名稱
return DynamicDataSourceHolder.getDataSource();
}
}
4、動態數據源持有者,負責利用ThreadLocal存取本線程使用的數據源的名稱
package com.hbis.mysql.config;
/**
* 動態數據源持有者,負責利用ThreadLocal存取本線程使用的數據源的名稱
*
* @author henry
* @date 2020/7/1 10:56
*/
public class DynamicDataSourceHolder {
/**
* 本地線程共享對象
*/
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void putDataSource(String name) {
THREAD_LOCAL.set(name);
}
static String getDataSource() {
return THREAD_LOCAL.get();
}
/**
* 清除本線程指定的數據源使用默認數據源
*/
public static void clear() {
THREAD_LOCAL.remove();
}
}
5、基於aop攔截 註解/java類
package com.hbis.mysql.aop;
import com.hbis.annotation.DataSource;
import com.hbis.mysql.config.DynamicDataSourceHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 數據源切換控制切面
*
* @auther: henry
*/
@Component
@Aspect
@Order(0) //保證先被執行
@Slf4j
public class DataSourceAspect {
/**
* 註解攔截切面表達式
*
* @annotation 用於攔截所有被該註解標註的方法
* @within 用於攔截被所有該註解標註的類
*/
@Pointcut("@annotation(com.hbis.annotation.DataSource) || @within(com.hbis.annotation.DataSource) ")
public void pointcut() {
}
@Before("pointcut()")
public void annotationMethodBefore(JoinPoint joinPoint) {
Class<?> clazz = joinPoint.getTarget().getClass();
DataSource annotation = clazz.getAnnotation(DataSource.class);
//先判斷類上是否有DataSource註解,如果沒有在判斷方法上是否有註解
if (annotation == null) {//類上沒有
//獲取方法上的註解
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
annotation = method.getAnnotation(DataSource.class);
//如果還是爲null則退出,這次方法調用將使用默認的數據源
if (annotation == null) {
return;
}
}
//獲取註解上得值
String dataSourceName = annotation.value();
log.info("---------------------------切換到數據源:" + dataSourceName + "----------------------------------");
//因爲有默認數據源的存在,所以不用擔心註解上的值無對應的數據源,當找不到指定數據源時,會使用默認的數據源
DynamicDataSourceHolder.putDataSource(dataSourceName);
}
//執行完切面後,將線程共享中的數據源名稱清空 讓程序使用默認數據源
@After("pointcut()")
public void annotationMethodAfter(JoinPoint joinPoint) {
DynamicDataSourceHolder.clear();
}
//---------------------------------基於AOP,攔截某種\某個類-------------------------------------------------------------
@Pointcut("execution( * com.hbis.framework.service.impl.*.*(..))")
public void pointcut2() {
}
@Before("pointcut2()")
public void dataSourcePointCut(JoinPoint joinPoint) {
Class<?> aClass = joinPoint.getTarget().getClass();
DataSource annotation = aClass.getAnnotation(DataSource.class);
//取出類上的註解,並將ThreadLocal 中的數據源設置爲指定的數據源, 當然 也可以按照業務需要不適用註解,直接固定某一數據源
if (annotation != null) {
String dataSource = annotation.value();
DynamicDataSourceHolder.putDataSource(dataSource);
}
}
@After("pointcut2()")
public void annotationMethodAfter1(JoinPoint joinPoint) {
DynamicDataSourceHolder.clear();
}
}
6、自定義註解
package com.hbis.annotation;
import com.hbis.constants.DBConstants;
import java.lang.annotation.*;
/**
* 用以切換數據源的註解
*
* @author henry
* @date 2020/6/19 14:14
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataSource {
String value() default DBConstants.DATASOURCE_MASTER;
}
7、數據庫使用常量類
package com.hbis.constants;
public class DBConstants {
/**
* 數據源分組
*/
public static final String DATASOURCE_SUPPLIER = "supplier";
/**
* 數據源分組
*/
public static final String DATASOURCE_MASTER = "master";
}
8、使用示列 基於註解!兩種方式二選一即可!
/*
* Copyright (c) 2018-2022 henry, .
*/
package com.hbis.businesscooperationmanager.service.impl;
import com.hbis.annotation.DataSource;
import com.hbis.businesscooperationmanager.dao.SupplierInfoDao;
import com.hbis.businesscooperationmanager.model.dto.BoSupplierContractVO;
import com.hbis.businesscooperationmanager.model.entity.SupplierInfo;
import com.hbis.businesscooperationmanager.service.SupplierInfoService;
import com.hbis.constants.DBConstants;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* <p>
* 供應商信息管理表 服務實現類
* </p>
*
* @author henry
*/
@Service
@DataSource(DBConstants.DATASOURCE_SUPPLIER)
public class SupplierInfoServiceImpl implements SupplierInfoService {
@Override
@DataSource(DBConstants.DATASOURCE_SUPPLIER)
public Map<String, SupplierInfo> querySupplierInfoByIds(List<String> ids) {
return baseDao.querySupplierInfoByIds(ids);
}
}