目的:
公司当前的后台项目经常会整合多数据源在同一个项目使用,我想要写一个公共的多数据源组件达到引用该组件即可自动装配多数据源的功能。
需求分析:
1.需要支持不同方法的数据源切换。在同一个类中可能会出现不同方法引用不同的数据源。
2.需要支持不同类的数据源切换。如果需要在每一个方法上面都定义具体使用的数据源类型,这种做法相对来说过于繁琐,所以希望可以达到class定义一次数据源类型成员方法都可以受用。
3.需要支持同一个方法实现不同数据源的切换。主要是为了支持数据库中存储的sql字段可以通过程序执行时达到数据源动态切换的目的。
编写组件:
1.多数据源注册:
创建DynamicDataSourceContextHolder类,配置数据源上下文
/**
* 存储已经注册的数据源的key
*/
protected static ListdataSourceIds =new ArrayList<>();
/**
* 线程级别的私有变量,每一个线程的私有量是独立的
*/
private static final ThreadLocalHOLDER =new ThreadLocal<>();
/**
* 获取当前使用的数据源key
*/
public static StringgetDataSourceRouterKey () {
return HOLDER.get();
}
/**
* 设置当前使用的数据源key
* @param dataSourceRouterKey
*/
public static void setDataSourceRouterKey (String dataSourceRouterKey) {
log.debug("change to {} datasource", dataSourceRouterKey);
HOLDER.set(dataSourceRouterKey);
}
/**
* 设置数据源之前一定要先移除
*/
public static void removeDataSourceRouterKey () {
HOLDER.remove();
}
/**
* 判断指定DataSrouce当前是否存在
*
* @param dataSourceId 数据源标识主键
* @return 是否成功
*/
public static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
创建DynamicDataSourceRegister类,实现多数据源注册
@Slf4j
public class DynamicDataSourceRegisterimplements ImportBeanDefinitionRegistrar, EnvironmentAware {
/**
* 配置上下文(也可以理解为配置文件的获取工具)
*/
private Environmentevn;
/**
* 别名
*/
private final static ConfigurationPropertyNameAliasesaliases =new ConfigurationPropertyNameAliases();
/**
* 由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况
*/
static {
aliases.addAliases("url", new String[]{"jdbc-url"});
aliases.addAliases("username", new String[]{"user"});
}
/**
* 存储我们注册的数据源
*/
private MapcustomDataSources =new HashMap();
/**
* 参数绑定工具 springboot2.0新推出
*/
private Binderbinder;
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
// 获取所有数据源配置
Map config, defauleDataSourceProperties =binder.bind("spring.datasource", Map.class).get();
// 获取数据源类型
String typeStr =evn.getProperty("spring.datasource.type");
// 获取数据源类型
Class clazz = getDataSourceType(typeStr);
// 绑定默认数据源参数 也就是主数据源
DataSource consumerDatasource, defaultDatasource = bind(clazz, defauleDataSourceProperties);
DynamicDataSourceContextHolder.dataSourceIds.add("master");
log.info("注册默认数据源成功");
// 获取其他数据源配置
List configs =binder.bind("custom.datasource", Bindable.listOf(Map.class)).get();
// 遍历从数据源
for (int i =0; i < configs.size(); i++) {
config = configs.get(i);
clazz = getDataSourceType((String) config.get("type"));
defauleDataSourceProperties = config;
// 绑定参数
consumerDatasource = bind(clazz, defauleDataSourceProperties);
// 获取数据源的key,以便通过该key可以定位到数据源
String key = config.get("key").toString();
customDataSources.put(key, consumerDatasource);
// 数据源上下文,用于管理数据源与记录已经注册的数据源key
DynamicDataSourceContextHolder.dataSourceIds.add(key);
log.info("注册数据源{}成功", key);
}
// bean定义类
GenericBeanDefinition define =new GenericBeanDefinition();
// 设置bean的类型,此处DynamicRoutingDataSource是继承AbstractRoutingDataSource的实现类
define.setBeanClass(DynamicRoutingDataSource.class);
// 需要注入的参数
MutablePropertyValues mpv = define.getPropertyValues();
// 添加默认数据源,避免key不存在的情况没有数据源可用
mpv.add("defaultTargetDataSource", defaultDatasource);
// 添加其他数据源
mpv.add("targetDataSources", customDataSources);
// 将该bean注册为datasource,不使用springboot自动生成的datasource
beanDefinitionRegistry.registerBeanDefinition("datasource", define);
log.info("注册数据源成功,一共注册{}个数据源", customDataSources.keySet().size() +1);
}
/**
* 绑定参数,以下三个方法都是参考DataSourceBuilder的bind方法实现的,目的是尽量保证我们自己添加的数据源构造过程与springboot保持一致
*/
private void bind(DataSource result, Map properties) {
ConfigurationPropertySource source =new MapConfigurationPropertySource(properties);
Binder binder =new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
// 将参数绑定到对象
binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
}
private T bind(Class clazz, Map properties) {
ConfigurationPropertySource source =new MapConfigurationPropertySource(properties);
Binder binder =new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
// 通过类型绑定参数并获得实例对象
return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
}
private T bind(Class clazz, String sourcePath) {
Map properties =binder.bind(sourcePath, Map.class).get();
return bind(clazz, properties);
}
/**
* EnvironmentAware接口的实现方法,通过aware的方式注入,此处是environment对象
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
log.info("开始注册数据源");
this.evn = environment;
// 绑定配置器
binder = Binder.get(evn);
}
/**
* 通过字符串获取数据源class对象
*
* @param typeStr
* @return
*/
private ClassgetDataSourceType(String typeStr) {
Class type;
try {
if (StringUtils.hasLength(typeStr)) {
// 字符串不为空则通过反射获取class对象
type = (Class) Class.forName(typeStr);
}else {
// 默认为hikariCP数据源,与springboot默认数据源保持一致
type = HikariDataSource.class;
}
return type;
}catch (Exception e) {
throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //无法通过反射获取class对象的情况则抛出异常,该情况一般是写错了,所以此次抛出一个runtimeexception
}
}
创建DynamicRoutingDataSource,实现动态路由
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource{
@Override
protected ObjectdetermineCurrentLookupKey() {
String dataSourceName = DynamicDataSourceContextHolder.getDataSourceRouterKey();
log.info("当前数据源是:{}", dataSourceName);
return dataSourceName;
}
}
2.多数据源切换:
利用注解和AOP实现多数据源之间的切换。
编写注解类
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE, ElementType.PARAMETER})
@Documented
public @interface TargetDataSource {
String value()default "master"; //数据源key
TargetDataSourceType type()default TargetDataSourceType.DEFAULT; //数据源key获取策略
}
public enum TargetDataSourceType {
DEFAULT,
DYNAMIC_METHOD_PARAMETER; //数据源类型取方法参数名
}
编写切面
@Order(1)
@Component
@Aspect
@Slf4j
/**
* 本切面类得目标是实现以下功能:
* 1.方法定义的数据源类型实现切换
* 2.类定义的数据源类型实现切换
* 3.同一个方法根据入参实现数据源切换
*/
public class DynamicDataSourceAspect {
@Before(value ="@annotation(targetDataSource)")
public void methodChangeDataSource(JoinPoint point, TargetDataSource targetDataSource)throws Throwable {
String datasourceId = targetDataSource.value();
TargetDataSourceType targetDataSourceType = targetDataSource.type();
if(targetDataSourceType!=null){
String className = point.getTarget().getClass().getName();
String methodName = point.getSignature().getName();
String[] paramNames = ReflexUtil.getFieldsName(className,methodName);
int dbIdx = Arrays.binarySearch(paramNames,"db");
if (dbIdx!=-1){
datasourceId = point.getArgs()[dbIdx].toString();
}
}
if (!DynamicDataSourceContextHolder.containsDataSource(datasourceId)) {
log.error("数据源[{}]不存在,使用默认数据源 > {}", datasourceId, point.getSignature());
}else {
log.debug("Use DataSource : {} > {}", datasourceId, point.getSignature());
DynamicDataSourceContextHolder.setDataSourceRouterKey(datasourceId);
}
}
@After(value ="@annotation(targetDataSource)")
public void methodRestoreDataSource(JoinPoint point, TargetDataSource targetDataSource) {
log.debug("Revert DataSource : {} > {}" + targetDataSource.value(), point.getSignature());
DynamicDataSourceContextHolder.removeDataSourceRouterKey();
}
@Before(value ="@within(targetDataSource)")
public void classChangeDataSource(JoinPoint point, TargetDataSource targetDataSource)throws Throwable {
String datasourceId = targetDataSource.value();
if (!DynamicDataSourceContextHolder.containsDataSource(datasourceId)) {
log.error("数据源[{}]不存在,使用默认数据源 > {}", datasourceId, point.getSignature());
}else {
log.debug("Use DataSource : {} > {}", datasourceId, point.getSignature());
DynamicDataSourceContextHolder.setDataSourceRouterKey(datasourceId);
}
}
@After(value ="@within(targetDataSource)")
public void classRestoreDataSource(JoinPoint point, TargetDataSource targetDataSource) {
log.debug("Revert DataSource : {} > {}" + targetDataSource.value(), point.getSignature());
DynamicDataSourceContextHolder.removeDataSourceRouterKey();
}
}
测试过程
1.引入多数据源组件,groupId和artifactId根据组件名称而定我的是叫下面这个。
<groupId>com.study.springboot</groupId>
<artifactId>dynamic-datasource
<version>1.0-SNAPSHOT
</dependency>
2.创建yml文件,如下配置了主数据源和CRM、CMP数据源,一共三个不同的数据源。
server:
port:8888
spring:
datasource:
driver: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/java2000?autoReconnect=true&useSSL=false&allowMultiQueries=true
username: root
password: root
type:com.zaxxer.hikari.HikariDataSource
custom:
datasource:
-key: CRM
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://localhost:1433;DatabaseName=MJNCRM
username: CRM#Java-Write
password: $33goLsjnFHe!
type: com.zaxxer.hikari.HikariDataSource
-key: CMP
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cmp?autoReconnect=true&useSSL=false&allowMultiQueries=true
username: awsuser
password: Ladder19
type: com.zaxxer.hikari.HikariDataSource
mybatis:
configuration:
mapUnderscoreToCamelCase: true
3.编写不同数据源的mapper,如下使用了三个不同数据源所编写的mapper
4.将注解应用于代码
@Service
//@TargetDataSource("CMP")
public class TestServiceextends ServiceImpl {
private final CRMMappercrmMapper;
private final MasterMappermasterMapper;
public TestService(CRMMapper crmMapper, MasterMapper masterMapper) {
this.crmMapper = crmMapper;
this.masterMapper = masterMapper;
}
//@TargetDataSource("CMP")
public void testCmp(){
List list =baseMapper.selectList(null);
list.forEach(item->{
System.out.println(item.getCampaignName());
});
}
//@TargetDataSource("CRM")
public void testCrm(){
List list =crmMapper.selectList(null);
list.forEach(item->{
System.out.println(item.getCellphone());
});
}
//@TargetDataSource("master")
public void testMaster(){
List list =masterMapper.selectList(null);
list.forEach(item->{
System.out.println(item.getStoreNo());
});
}
@TargetDataSource(type = TargetDataSourceType.DYNAMIC_METHOD_PARAMETER)
public void test4(String db,int choose){
switch (choose){
case 1:
testCmp();
break;
case 2:
testCrm();
break;
case 3:
testMaster();
break;
}
}
}
参考引用
在编写多数据源的组件时参考了https://blog.csdn.net/xp541130126/article/details/81739760和同事Evan所编写的代码。感谢两位!!!第一次写,写的不好请多给予批评和建议,谢谢大家