要实现动态数据源切换,离不开Spring框架的AbstractRoutingDataSource这个抽象类。这是实现动态数据源切换的关键。我们先看下这个抽象类。
AbstractRoutingDataSource的源码如下:
- //
- // Source code recreated from a .class file by IntelliJ IDEA
- // (powered by Fernflower decompiler)
- //
-
- package org.springframework.jdbc.datasource.lookup;
-
- import java.sql.Connection;
- import java.sql.SQLException;
- import java.util.Collections;
- import java.util.Map;
- import javax.sql.DataSource;
- import org.springframework.beans.factory.InitializingBean;
- import org.springframework.jdbc.datasource.AbstractDataSource;
- import org.springframework.lang.Nullable;
- import org.springframework.util.Assert;
- import org.springframework.util.CollectionUtils;
-
- public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
- @Nullable
- private Map
- @Nullable
- private Object defaultTargetDataSource;
- private boolean lenientFallback = true;
- private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
- @Nullable
- private Map
- @Nullable
- private DataSource resolvedDefaultDataSource;
-
- public AbstractRoutingDataSource() {
- }
-
- public void setTargetDataSources(Map {
- this.targetDataSources = targetDataSources;
- }
-
- public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
- this.defaultTargetDataSource = defaultTargetDataSource;
- }
-
- public void setLenientFallback(boolean lenientFallback) {
- this.lenientFallback = lenientFallback;
- }
-
- public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
- this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
- }
-
- public void afterPropertiesSet() {
- if (this.targetDataSources == null) {
- throw new IllegalArgumentException("Property 'targetDataSources' is required");
- } else {
- this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
- this.targetDataSources.forEach((key, value) -> {
- Object lookupKey = this.resolveSpecifiedLookupKey(key);
- DataSource dataSource = this.resolveSpecifiedDataSource(value);
- this.resolvedDataSources.put(lookupKey, dataSource);
- });
- if (this.defaultTargetDataSource != null) {
- this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
- }
-
- }
- }
-
- protected Object resolveSpecifiedLookupKey(Object lookupKey) {
- return lookupKey;
- }
-
- protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
- if (dataSource instanceof DataSource) {
- return (DataSource)dataSource;
- } else if (dataSource instanceof String) {
- return this.dataSourceLookup.getDataSource((String)dataSource);
- } else {
- throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
- }
- }
-
- public Map
- Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet");
- return Collections.unmodifiableMap(this.resolvedDataSources);
- }
-
- @Nullable
- public DataSource getResolvedDefaultDataSource() {
- return this.resolvedDefaultDataSource;
- }
-
- public Connection getConnection() throws SQLException {
- return this.determineTargetDataSource().getConnection();
- }
-
- public Connection getConnection(String username, String password) throws SQLException {
- return this.determineTargetDataSource().getConnection(username, password);
- }
-
- public
T unwrap(Class iface) throws SQLException { - return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
- }
-
- public boolean isWrapperFor(Class> iface) throws SQLException {
- return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
- }
-
- protected DataSource determineTargetDataSource() {
- Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
- Object lookupKey = this.determineCurrentLookupKey();
- DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
- if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
- dataSource = this.resolvedDefaultDataSource;
- }
-
- if (dataSource == null) {
- throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
- } else {
- return dataSource;
- }
- }
-
- @Nullable
- protected abstract Object determineCurrentLookupKey();
- }
首先,这个类AbstractRoutingDataSource这个抽象类是继承了AbstractDataSource抽象类,并且实现了InitializingBean接口。InitializingBean接口提供了一个afterPropertiesSet()方法,凡是继承了该接口的类那么在初始化Bean的时候就会执行该方法。所以AbstractRoutingDataSource类中的afterPropertiesSet正是用于初始化一些信息。要想了解InitializingBean这个接口,可以参考我的这篇blog:Spring框架中InitializingBean的作用
其次,这个类AbstractRoutingDataSource类有个抽象方法,这个抽象方法需要我们实现,这个方法的作用就是指定用哪个数据源,也就是告诉dao层现在连接数据源用我们指定的数据源了。
protected abstract Object determineCurrentLookupKey();
AbstractRoutingDataSource还继承了抽象类AbstractDataSource,而AbstractDataSource抽象类又继承了DataSource接口,在DataSource中只有两个方法,这两个方法会在AbstractDataSource抽象类中得到具体实现。
现在看看AbstractRoutingDataSource中的一些属性都有什么作用
- @Nullable
- private Map
- @Nullable
- private Object defaultTargetDataSource; 默认数据源,如果想指定默认数据源,可以给它赋值
- private boolean lenientFallback = true;
- private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
- @Nullable
- private Map
- @Nullable
- private DataSource resolvedDefaultDataSource; 默认数据源,将defaultTargetDataSource转为DataSource赋值给resolvedDefaultDataSource
-
- public AbstractRoutingDataSource() {
- }
AbstractRoutingDataSource抽象类中afterPropertiesSet()方法是核心,那看看他到底做了哪些初始化操作。
- public void afterPropertiesSet() {
- if (this.targetDataSources == null) {
- throw new IllegalArgumentException("Property 'targetDataSources' is required");
- } else {
- this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
- this.targetDataSources.forEach((key, value) -> {
- Object lookupKey = this.resolveSpecifiedLookupKey(key);
- DataSource dataSource = this.resolveSpecifiedDataSource(value);
- this.resolvedDataSources.put(lookupKey, dataSource);
- });
- if (this.defaultTargetDataSource != null) {
- this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
- }
-
- }
- }
第一,当targetDataSources为空时,会抛出IllegalArgumentException错误,所以我们在配置多数据源时,至少需要传入一个数据源。
第二, 初始化了resolvedDataSources大小,resolvedDataSources只是把targetDataSources的内容copy了一份,不同之处在于targetDataSources的value是Object类型,而resolvedDataSources的value是DataSource类型。
第三,遍历targetDataSources集合,然后调用了resolveSpecifiedLookupKey()方法和resolveSpecifiedDataSource()方法,最后将返回值当作Key-Value放入resolvedDataSources集合中。
第四,那么方法resolveSpecifiedLookupKey()和方法resolveSpecifiedDataSource()分别做了什么呢?我们可以查看两个方法的实现。其实可以看到resolveSpecifiedLookupKey并没有做什么操作就直接返回了值,而resolveSpecifiedDataSource只是把Object转为DataSource对象返回。
所以afterPropertiesSet()初始话就是将targetDataSources的内容转化一下放到resolvedDataSources中,将defaultTargetDataSource转为DataSource赋值给resolvedDefaultDataSource。
我们再看getConnection()方法是如何实现的。调用了determineTargetDataSource()方法。
- public Connection getConnection() throws SQLException {
- return this.determineTargetDataSource().getConnection();
- }
那么determineTargetDataSource方法到底做了什么,我们可以看一下determineTargetDataSource的实现。该方法返回DataSource类型的对象,并且调用了determineCurrentLookupKey方法,可能这时有人发现了这个方法就是我们自定义数据源类要实现的那个方法。determineCurrentLookupKey返回一个Object,命名为lookupKey,将lookupKey作为key,到resolvedDataSources集合中去拿数据源,如果没有拿到数据源,那么它会拿默认数据源resolvedDefaultDataSource。如果还是没有拿到,此时就报错啦!
通过上面的分析,我们自己实现的方法determineCurrentLookupKey()是什么时候别调用的呢?从逻辑上看determineCurrentLookupKey()被determineTargetDataSource()调用,而determineTargetDataSource()被getConnection()调用,那getConnection()方法是被谁在什么时候调用的呢?通过代码跟踪,是我们在service层调用dao层方法时,这时候调用的。
了解了AbstractRoutingDataSource这个抽象类,也就该实现一个动态数据源切换功能了,涉及的源码如下:
- package com.lsl.mylsl.config;
-
- import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
-
- /**
- * 动态数据源实现类
- */
- public class DynamicDataSource extends AbstractRoutingDataSource {
-
-
- @Override
- protected Object determineCurrentLookupKey() {
- String dataSource = DBContextHolder.getDataSource();
- if (dataSource == null || "".equals(dataSource)){
- dataSource = "ds1";
- }
- return dataSource;
- }
- }
当没有指定数据源时,默认指定ds1为当前数据源
- package com.lsl.mylsl.config;
-
- public class DBContextHolder {
-
- private static final ThreadLocal
CONTEXT_HOLDER = new ThreadLocal<>(); -
- /**
- * 动态切换数据源
- * @param dataSource
- */
- public static void setDataSource(String dataSource){
-
- if (SpringbootConfig.DBMAP.containsKey(dataSource)){
- CONTEXT_HOLDER.set(dataSource);
- }else {
- System.out.println("数据源" + dataSource + "不存在");
- }
- }
-
- /**
- * 获取数据源
- * @return
- */
- public static String getDataSource(){
- return CONTEXT_HOLDER.get();
- }
-
- /**
- * 清空数据源
- */
- public static void clearDataSource(){
- CONTEXT_HOLDER.remove();
- }
- }
- package com.lsl.mylsl.config;
-
- import com.alibaba.druid.pool.DruidDataSource;
- import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
- 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.sql.DataSource;
- import java.util.HashMap;
- import java.util.Map;
-
- /**
- * 初始化多个数据源
- * 注册动态数据源
- */
- @Configuration
- public class SpringbootConfig {
-
- public static final Map
-
- /**
- * 初始化数据源1
- */
- public void initDataSource1(){
- try {
- DruidDataSource dds1 = DruidDataSourceBuilder.create().build();
- dds1.setUsername("admin");
- dds1.setPassword("123456");
- dds1.setUrl("jdbc:oracle:thin:@10.10.10.10:1521/oracle1");
- dds1.setInitialSize(5);
- dds1.setMinIdle(5);
- dds1.setMaxActive(20);
- dds1.setMaxWait(60000);
- dds1.setTimeBetweenEvictionRunsMillis(60000);
- dds1.setMinEvictableIdleTimeMillis(300000);
- dds1.setValidationQuery("SELECT * FROM DUAL");
- dds1.setTestWhileIdle(true);
- dds1.setTestOnBorrow(false);
- dds1.setTestOnReturn(false);
- dds1.setMaxPoolPreparedStatementPerConnectionSize(20);
- dds1.setFilters("stat,wall");
- dds1.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slow.slowSqlMillis=5000");
- dds1.setUseGlobalDataSourceStat(true);
- //添加数据源到map
- SpringbootConfig.DBMAP.put("ds1",dds1);
- } catch (Exception e) {
-
- }
-
-
- }
-
- /**
- * 初始化数据源2
- */
- public void initDataSource2(){
- try {
- DruidDataSource dds2 = DruidDataSourceBuilder.create().build();
- dds2.setUsername("admin");
- dds2.setPassword("123456");
- dds2.setUrl("jdbc:mysql://10.10.10.10:1521/mysql1");
- dds2.setInitialSize(5);
- dds2.setMinIdle(5);
- dds2.setMaxActive(20);
- dds2.setMaxWait(60000);
- dds2.setTimeBetweenEvictionRunsMillis(60000);
- dds2.setMinEvictableIdleTimeMillis(300000);
- dds2.setValidationQuery("SELECT 1");
- dds2.setTestWhileIdle(true);
- dds2.setTestOnBorrow(false);
- dds2.setTestOnReturn(false);
- dds2.setMaxPoolPreparedStatementPerConnectionSize(20);
- dds2.setFilters("stat,wall");
- dds2.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slow.slowSqlMillis=5000");
- dds2.setUseGlobalDataSourceStat(true);
- dds2.setDbType("mysql");
- //添加数据源到map
- SpringbootConfig.DBMAP.put("ds2",dds2);
- } catch (Exception e) {
-
- }
- }
-
- /**
- * 把DynamicDataSource交给spring托管
- * @return
- */
- @Bean
- public DataSource dynamicDataSource(){
- DynamicDataSource myDs = new DynamicDataSource();
- initDataSource1();
- initDataSource2();
- myDs.setTargetDataSources(SpringbootConfig.DBMAP);
- myDs.afterPropertiesSet();
- return myDs;
- }
-
- /**
- * 事务管理
- * @return
- */
- @Bean
- public PlatformTransactionManager transactionManager(){
- return new DataSourceTransactionManager(dynamicDataSource());
- }
- }
如何实现数据源切换,代码很简单,如下:
- package com.lsl.mylsl.service.impl;
-
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.lsl.mylsl.BO.CatBO;
- import com.lsl.mylsl.config.DBContextHolder;
- import com.lsl.mylsl.mapper.CatMapper;
- import com.lsl.mylsl.service.ICatService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- import java.util.List;
- import java.util.Map;
-
- @Service
- public class CatServiceImpl extends ServiceImpl
implements ICatService { -
- @Autowired
- CatMapper catMapper;
-
- @Override
- public List
- //切换数据源到ds2
- DBContextHolder.setDataSource("ds2");
- return catMapper.qryCatByAge(params);
- }
- }