一、前言
本文是笔者工作过程中突发奇想缩写,由于本人技术水平有限,在文章中难免出现错误,如有发现,感谢各位指正。在阅读过程中也创建了一些衍生文章,衍生文章的意义是因为自己在看源码的过程中,部分知识点并不了解或者对某些知识点产生了兴趣,所以为了更好的阅读源码,所以开设了衍生篇的文章来更好的对这些知识点进行进一步的学习。
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 初始化流程:
- 校验 targetDataSources 是否为空,为空抛出异常。
- 解析 targetDataSources ,并将解析结果放入resolvedDataSources 中。对于 targetDataSources 和 resolvedDataSources 来说,其中每个DataSource 对应一个 唯一的key。
- 如果指定了默认数据源,则解析后赋值给 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);
}
四、总结
动态数据源的整个流程很简单:
- Spring 在获取数据库连接时会调用 DataSource#getConnection 方法。而对于动态数据源,我们注入的是DataSource 为 AbstractRoutingDataSource 子类,需要注意的是,AbstractRoutingDataSource 在初始化的时候需要指定候选的数据源集合, AbstractRoutingDataSource 中会以 key:value 的形式保存这些候选的数据源集合。
- 当通过 AbstractRoutingDataSource#getConnection 来获取数据库连接时, AbstractRoutingDataSource#getConnection 方法中会通过 AbstractRoutingDataSource#determineCurrentLookupKey 方法获取key,并根据key值获取到对应的数据源,并从中获取到数据库连接。
以上:内容部分参考
https://www.jianshu.com/p/b2f818b742a2
https://blog.csdn.net/hacker_lees/article/details/70005984
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正