解析配置文件自动装配 DataSource + AbstractRoutingDataSource + AOP 实现动态数据源 上:原理解析,解析数据源

spring boot 自动装配会通过 spring.datasource.*为我们自动装配数据源,所以想要动态的切换数据源,第一件事是配置数据源,其次是怎么切换?最后何时切换?

原理解析(使用 AbstractRoutingDataSource 实现)

spring-jdbc 提供了 AbstractRoutingDataSourcegetConnection() 时通过 lookup key决定目标数据源,使用 AbstractRoutingDataSource 需要准备至少两个数据源,这在源码中也有体现:

一个默认数据源 + 动态匹配的数据源,resolvedDataSources 是一个 Object 为 key,DataSource为 value 的 Map,可见这里 key 即充当了 lookup key的角色。
image.png
在调用 getConnection 获取数据源时会调用 determineTargetDataSource 方法获取目标数据源,进而通过 determineCurrentLookupKey 方法获得当前的 lookup key,再从 resolvedDataSources 中获得目标数据源。

AbstractRoutingDataSource 是一个抽象类,determineCurrentLookupKey是其唯一的抽象方法,意味着子类只需在适当的时候修改当前的 lookup key 就能实现动态的更改当前的数据源。

配置数据源

配置数据源也就是给 AbstractRoutingDataSourcedefaultTargetDataSourceresolvedDataSources 进行赋值。

这里我们将数据源配置在 properties 文件中,通过工具类解析进行配置。

在 properties 中配置数据源

demo 项目为 spring boot 项目,配置文件采用 yml 格式:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: 1234
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    multi:
      ds-keys: db1,db2
      db1:
        url: jdbc:mysql://127.0.0.1:3306/db11?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 1234
        driver-class-name: com.mysql.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
      db2:
        url: jdbc:mysql://127.0.0.1:3306/db12?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 1234
        driver-class-name: com.mysql.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource

spring.datasource 前缀的将被解析为默认数据源, spring.datasource.multi 前缀的则作为动态数据源进行解析。
spring.datasource.multi.ds-keys 是比较关键的属性,这个参数定义了项目中所有的动态数据源的 key,如上述代码所示,spring.datasource.multi.ds-keys: db1,db2:表明共有两个动态数据源,key 分别为 db1 和 db2 ,进行解析时将解析如下两个前缀配置的数据源:

  • spring.datasource.multi.db1
  • spring.datasource.multi.db2

当然这个规则是我自己定义的,可以按需定义自己的规则并实现相应的解析。

解析数据源

使用 DynamicDataSourceBuilder 类来解析数据源,注册为 bean 并实现 EnvironmentAware 接口,在 setEnvironment 方法中开始进行解析。
image.png

initDefaultDataSourceinitCustomDataSources 方法将解析得到默认数据源和动态数据源,默认数据源将赋值给 defaultDataSourcetargetDataSources 中是所有解析得到的数据源,包括默认数据源,其数据类型是一个 Map,Map 的 key 即为 Lookup key

初始化动态数据源
    /**
     * 初始化定制数据源
     */
    private void initCustomDataSources(Environment env) {

        // 读取配置文件获取定制数据源,也可以通过数据库获取数据源
        String dsNames = env.getProperty(customDataSourceKeys);

        for (String dsKey : dsNames.split(",")) {
            DataSource ds = buildDataSource(env, customDataSourcePrefix + "." + dsKey);
            targetDataSources.put(dsKey, ds);
            dataBinder(ds, env);
        }

    }

customDataSourceKeys 即为 spring.datasource.multi.ds-keys,得到定制数据源前缀后进行构建,之后添加到 targetDataSources 中。

构建数据源

    /**
     * 创建 datasource.
     */
    @SuppressWarnings("unchecked")
    private DataSource buildDataSource(Environment env, String dsPrefix) {

        try {

            String prefix = dsPrefix + ".";
            String dbpType = env.getProperty(prefix + DataSourcePropertyKey.type, defaultDataSourceType);
            Class<? extends DataSource> dsType = (Class<? extends DataSource>) Class.forName(dbpType);

            DataSource dataSource = DataSourceBuilder.create()
                    .driverClassName(env.getProperty(prefix + DataSourcePropertyKey.driverClassName))
                    .url(env.getProperty(prefix + DataSourcePropertyKey.url))
                    .username(env.getProperty(prefix + DataSourcePropertyKey.username))
                    .password(env.getProperty(prefix + DataSourcePropertyKey.password))
                    .type(dsType)
                    .build();

            configDataSourcePool(dataSource);
            return dataSource;

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

    }

数据源的构建通过读取 properties 文件,并借助 DataSourceBuilder 类进行构建。configDataSourcePool 方法可对数据源对应的连接池进行配置。

这里需要注意的是 spring boot 2.* 对配置文件的读取 API 有比较大的变动,可参考这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
下面是使用 AbstractRoutingDataSource 和 MyBatis 拦截器实现动态切换数据源的示例代码: 首先,需要自定义一个继承 AbstractRoutingDataSource 的类,并实现 determineCurrentLookupKey 方法,该方法用于返回当前数据源的 key: ```java public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>(); public static void setDataSourceKey(String key) { dataSourceKey.set(key); } @Override protected Object determineCurrentLookupKey() { return dataSourceKey.get(); } } ``` 在 Spring 配置文件中需要配置两个数据源,并将 DynamicDataSource 设置为默认数据源: ```xml <bean id="dataSource1" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db1"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="dataSource2" class="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db2"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="dynamicDataSource" class="com.example.DynamicDataSource"> <property name="defaultTargetDataSource" ref="dataSource1"/> <property name="targetDataSources"> <map> <entry key="db1" value-ref="dataSource1"/> <entry key="db2" value-ref="dataSource2"/> </map> </property> </bean> ``` 接下来,需要实现一个继承于 MyBatis 的 Interceptor 接口的拦截器类,该类用于在执行 SQL 语句前切换数据源: ```java @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DynamicDataSourceInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); String dataSourceKey = getDataSourceKey(mappedStatement); if (dataSourceKey != null) { DynamicDataSource.setDataSourceKey(dataSourceKey); } return invocation.proceed(); } private String getDataSourceKey(MappedStatement mappedStatement) { String dataSourceKey = null; // 从 Mapper 方法上获取数据源 key if (mappedStatement != null) { String id = mappedStatement.getId(); if (id.startsWith("com.example.mapper1")) { dataSourceKey = "db1"; } else if (id.startsWith("com.example.mapper2")) { dataSourceKey = "db2"; } } return dataSourceKey; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // do nothing } } ``` 最后,需要在 Spring 配置文件中配置该拦截器: ```xml <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dynamicDataSource"/> <property name="plugins"> <array> <bean class="com.example.DynamicDataSourceInterceptor"/> </array> </property> </bean> ``` 这样,就可以在 Mapper 方法上使用 @DataSource("db1") 或 @DataSource("db2") 注解来指定使用哪个数据源了。例如: ```java @DataSource("db1") List<User> getUserList(); @DataSource("db2") int addUser(User user); ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值