Spring源码分析二十三:动态数据源的实现

一、前言

本文是笔者工作过程中突发奇想缩写,由于本人技术水平有限,在文章中难免出现错误,如有发现,感谢各位指正。在阅读过程中也创建了一些衍生文章,衍生文章的意义是因为自己在看源码的过程中,部分知识点并不了解或者对某些知识点产生了兴趣,所以为了更好的阅读源码,所以开设了衍生篇的文章来更好的对这些知识点进行进一步的学习。


Spring全集目录:Spring源码分析:全集整理

二、演示Demo

因为Demo不是本文重点,并且经历所限( = ),所以Demo写的很简单,搭建过程中部分内容参考了https://blog.csdn.net/hacker_lees/article/details/70005984 的内容。


application.yml 配置

# mybatis 配置
mybatis:
  mapper-locations: classpath*:mapper/*/*.xml #注意:一定要对应mapper映射xml文件的所在路径
  type-aliases-package: com.kingfish.dao  # 注意:对应实体类的路径
  
# 自定义动态数据源配置,可在这里自动增删数据源(因为懒这里就写)
dynamic:
  datasources:
    - name: master
      url: jdbc:mysql://ip:port/demo?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
      username: root
      password: 123456
      driverClassName: com.mysql.jdbc.Driver
    - name: slave
      url: jdbc:mysql://ip:port/demo?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
      username: root
      password: 123456
      driverClassName: com.mysql.jdbc.Driver

DynamicProperties :用于读取 上面 yml 的 dynamic 的 配置

@Data
@Component
@ConfigurationProperties(prefix = "dynamic")
public class DynamicProperties {
    // 这里直接使用 DataSourceProperties 来接受 yml 中的配置
    private List<DataSourceProperties> datasources;
//    map 形式也可以接收属性
//    private List<Map<String, String>> datasources;
}

DynamicDataSource :必须继承 AbstractRoutingDataSource 抽象类。用于完成动态数据源切换

public class DynamicDataSource extends AbstractRoutingDataSource {
	// 根据返回值切换 数据源
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSourceKey();
    }
}

DynamicDataSourceHolder :持有每个线程(请求)所要切换的数据源信息

public class DynamicDataSourceHolder {

    //写库对应的数据源key
    public static final String MASTER = "master";

    //读库对应的数据源key
    public static final String SLAVE = "slave";

    //使用ThreadLocal记录当前线程的数据源key
    private static final ThreadLocal<String> holder = new ThreadLocal<String>();

    private static final String[] slaveOperation = new String[]{"find", "get", "query", "list"};
    
    /**
     * 设置数据源key
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        holder.set(key);
    }

    /**
     * 获取数据源key
     *
     * @return
     */
    public static String getDataSourceKey() {
        return holder.get();
    }

    public static void removeDataSource() {
        holder.remove();
    }

    public static void dynamicMark(String methodName) {
        if (StringUtils.startsWithAny(methodName.toLowerCase(Locale.ROOT), slaveOperation)) {
            setDataSourceKey(SLAVE);
        } else {
            setDataSourceKey(MASTER);
        }
    }
    
}

DynamicDataSourceAop :动态数据源 AOP,用于切换数据源

// 自定义 切换数据源的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface Dynamic {
    String value() default "";
}


@Aspect
@Component
public class DynamicDataSourceAop {

    @Pointcut("execution(* com.kingfish.service.impl.*.*(..))")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
		// 获取 Dynamic  注解
        Dynamic dynamic = method.getAnnotation(Dynamic.class);
        if (dynamic == null) {
            Class<?> targetClass = point.getTarget().getClass();
            dynamic = targetClass.getAnnotation(Dynamic.class);
            if (dynamic == null) {
                for (Class<?> targetInterface : targetClass.getInterfaces()) {
                    dynamic = targetInterface.getAnnotation(Dynamic.class);
                }
            }
        }
        if (dynamic == null) {
        	// 如果没有 Dynamic 注解,则按照默认规则切换数据源
            DynamicDataSourceHolder.dynamicMark(method.getName());
        } else {
        	// 否则按照指定的数据源切换
            DynamicDataSourceHolder.setDataSourceKey(dynamic.value());
        }
    }
	// 方法执行结束,清除当前线程的数据源信息
    @After("pointCut()")
    public void after() {
        DynamicDataSourceHolder.removeDataSource();
    }

}

DataSourceConfig : 数据源的配置类

@Configuration
public class DataSourceConfig {
    @Autowired
    private DynamicProperties dynamicProperties;

    @Bean
    public DynamicDataSource dynamicDataSource() {
        Map<Object, Object> map = Maps.newHashMap();
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 解析 DynamicDataSource。转换成 DataSource
        dynamicProperties.getDatasources().forEach(properties -> {
            map.put(properties.getName(), properties.initializeDataSourceBuilder().build());
        });
        dynamicDataSource.setTargetDataSources(map);
        if (map.containsKey(DynamicDataSourceHolder.MASTER)) {
            dynamicDataSource.setDefaultTargetDataSource(map.get(DynamicDataSourceHolder.MASTER));
        }
        return dynamicDataSource;
    }

    @Bean
    public SqlSessionFactory dynamicSqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致)
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate dynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

测试相关

@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("selectAll")
    public Object selectAll() {
        return userService.selectAll();
    }
}


@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
	// 读走从库
    @Dynamic("slave")
    @Override
    public Object selectAll() {
        return userMapper.selectAll();
    }
    // 写走主库
    @Override
    public int insert(User user) {
        return userMapper.insert(user);
    }
}
// user 表对应 model
@Data
public class User {
    private Integer id;
    private String name;
}

@Mapper
public interface UserMapper {
    @Select("select * from user")
    List<User> selectAll();
    
    @Insert("insert into user(name) value(#{user.name})")
    int insert(@Param("user") User user);
}

三、源码分析

Springboot 中动态数据源的实现的核心类是 AbstractRoutingDataSource。AbstractRoutingDataSource 的结构如下:

在这里插入图片描述

这里我们注意到了 AbstractRoutingDataSource 实现了 InitializingBean 接口,这就注定了我们需要去看一看 AbstractRoutingDataSource#afterPropertiesSet方法的实现。

不过在此之前,我们先看一下 AbstractRoutingDataSource 中的属性,如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
	/***************** 1. 相关属性  ****************/
	// 1. 目标数据源。通过相应的 sett 方法进行赋值,在 afterPropertiesSet 中有校验不可为空。
	// 需要注意,此时的 value并不一定是 DataSource 类型。targetDataSources 经过解析后会保存到 resolvedDataSources 中
	@Nullable
	private Map<Object, Object> targetDataSources;
	// 2. 默认数据源, 可通过相应的 sett 方法进行赋值
	@Nullable
	private Object defaultTargetDataSource;
	// 3. 是否启用宽容规则 :如果为true,当找不到合适的数据源时会使用默认的数据源
	private boolean lenientFallback = true;
	// 4. 查找数据源的策略接口
	// 当argetDataSources 中的value 为 String时使用该属性用于查找对应的DataSource
	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
	// 5. 解析后的数据源集合
	@Nullable
	private Map<Object, DataSource> resolvedDataSources;
	// 6. 解析的默认的数据源,由defaultTargetDataSource 解析而来。
	@Nullable
	private DataSource resolvedDefaultDataSource;
	...
}

了解了上述属性的作用后,我们来看 AbstractRoutingDataSource#afterPropertiesSet 的实现 :

	@Override
	public void afterPropertiesSet() {
		// 如果目标数据源为空则直接抛出异常
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
		// 对 targetDataSources  进行解析,解析后的结果保存到resolvedDataSources 中。
		this.targetDataSources.forEach((key, value) -> {
			// 获取数据源的 key。默认直接将 key返回。
			Object lookupKey = resolveSpecifiedLookupKey(key);
			// 对value 进行解析,获取到 DataSource 保存到 resolvedDataSources中
			DataSource dataSource = resolveSpecifiedDataSource(value);
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		// 对默认数据源的解析
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}
	// 解析数据源的key
	protected Object resolveSpecifiedLookupKey(Object lookupKey) {
		return lookupKey;
	}
	
	// 解析数据源
	protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
		//如果value 直接是 DataSource类型则直接返回即可
		if (dataSource instanceof DataSource) {
			return (DataSource) dataSource;
		}
		// 如果是 String类型,则通过 dataSourceLookup (默认实现是JndiDataSourceLookup) 根据 value 去寻找
		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);
		}
	}

我们这边整理一下 AbstractRoutingDataSource 初始化流程:

  1. 校验 targetDataSources 是否为空,为空抛出异常。
  2. 解析 targetDataSources ,并将解析结果放入resolvedDataSources 中。对于 targetDataSources 和 resolvedDataSources 来说,其中每个DataSource 对应一个 唯一的key。
  3. 如果指定了默认数据源,则解析后赋值给 resolvedDefaultDataSource。

我们这里需要注意:
由于AbstractRoutingDataSource 在 afterPropertiesSet 方法中对 targetDataSources 中进行校验。

  • 如果Bean 通过 @Component 注解修饰注入,afterPropertiesSet 会在 Bean 调用构造函数后调用,所以我们可以在构造函数中对 defaultTargetDataSource 赋值 或重写 afterPropertiesSet 方法进行赋值。

        public DynamicDataSource() {
            Map<Object, Object> map = Maps.newHashMap();
            map.put("master", masterDataSource());
            map.put("slave", slaveDataSource());
            setDefaultTargetDataSource(masterDataSource());
            setTargetDataSources(map);
        }
        // 或者重写 afterPropertiesSet 方法,在最后调用 super.afterPropertiesSet();
        @Override
        public void afterPropertiesSet() {
            Map<Object, Object> map = Maps.newHashMap();
            map.put("master", masterDataSource());
            map.put("slave", slaveDataSource());
            setDefaultTargetDataSource(masterDataSource());
            setTargetDataSources(map);
            super.afterPropertiesSet();
        }
    
  • 如果Bean 通过 @Bean 修饰注入, afterPropertiesSet 会在 获取到 @Bean 方法返回的结果后调用。所以我们可以在 @Bean 方法中完成 defaultTargetDataSource 的赋值。其中 DynamicDataSource 是AbstractRoutingDataSource 的自定义子类。

        @Bean
        public DynamicDataSource dynamicDataSource() {
            Map<Object, Object> map = Maps.newHashMap();
            map.put("master", masterDataSource());
            map.put("slave", slaveDataSource());
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
            dynamicDataSource.setTargetDataSources(map);
            return dynamicDataSource;
        }
    

当服务进行数据库操作时,Spring会从容器中获取到 DataSource 实现类并调用 DataSource#getConnection 方法来获取数据库连接。而为了实现动态数据源,我们注入Spring容器的DataSource 实现类为 AbstractRoutingDataSource 的子类.AbstractRoutingDataSource#getConnection 则是通过 determineTargetDataSource 方法来选择合适的数据源。如下:

	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		// 此方法供子类实现,用于获取当前获取的数据源的 key,会根据此key来从resolvedDataSources 中获取数据源
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		// 如果没有获取到对应数据源 && (开启宽容后备 || lookupKey == null)。则使用默认的数据源
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}
	// 供子类实现,获取数据源的key
	protected abstract Object determineCurrentLookupKey();
	
	// 从数据源中获取连接
	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}
	
	// 从数据源中获取连接
	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return determineTargetDataSource().getConnection(username, password);
	}

四、总结

动态数据源的整个流程很简单:

  1. Spring 在获取数据库连接时会调用 DataSource#getConnection 方法。而对于动态数据源,我们注入的是DataSource 为 AbstractRoutingDataSource 子类,需要注意的是,AbstractRoutingDataSource 在初始化的时候需要指定候选的数据源集合, AbstractRoutingDataSource 中会以 key:value 的形式保存这些候选的数据源集合。
  2. 当通过 AbstractRoutingDataSource#getConnection 来获取数据库连接时, AbstractRoutingDataSource#getConnection 方法中会通过 AbstractRoutingDataSource#determineCurrentLookupKey 方法获取key,并根据key值获取到对应的数据源,并从中获取到数据库连接。

以上:内容部分参考
https://www.jianshu.com/p/b2f818b742a2
https://blog.csdn.net/hacker_lees/article/details/70005984
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值