MyBatis拦截器 Interceptor 实现多数据源切换

实现思路

        对于无事务的数据库 DQL操作,检查其 sqlCommandType、是否使用了 for update、或者检查是否存在指定注解。

前置知识

        AbstractRoutingDataSource

http://t.csdn.cn/n68tvhttp://t.csdn.cn/n68tv数据源配置与切换

public enum DataSources {
    // 主库-读数据源
    master,
    // 主库-写数据源
    slave,
    // 多数据源
    readmore
}
package com.gateway.admin.datasources;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;

/**
 * 数据源路由器
 */
public class DataSourceRouter extends AbstractRoutingDataSource {
    // 也可以指定 ThreadLocal 的 initialValue 的具体实现
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    /**
     *将targetDataSources映射中指定的给定查找键对象解析为用于与当前查找键匹配的实际查找键。
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return contextHolder.get();
    }
    // 设置数据源,参数类型是上面生命的枚举
    public static void setDataSource(DataSources dataSource) {
        contextHolder.set(dataSource);
    }

    // 在 finally 中及时 remove    
    public static void clearDataSource() {
        contextHolder.remove();
    }

}

package com.gateway.admin.datasources;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 多数据源配置类
 */
@Configuration
public class DynamicDataSourceConfig {

	//如果ioc容器中,同一个类型有多个bean,则bean的名称为方法的名称
    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.three")
    public DataSource threeDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.four")
    public DataSource fourDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean("dataSource")
    @Primary
    public DataSourceRouter dataSource(DataSource firstDataSource, DataSource secondDataSource, DataSource threeDataSource, DataSource fourDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceNames.master, master);
        targetDataSources.put(DataSourceNames.slave, slave);
        targetDataSources.put(DataSourceNames.readmore, readmore);
        
        // DataSourceNames.master, master 设置为默认数据源头
        return new DataSourceRouter(firstDataSource, targetDataSources);
    }
}

yml 配置

spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        druid:
            first:  #db1
                url: jdbc:mysql://127.0.0.1:3306/db1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
                username: root
                password: root
            second:  #db2
                url: jdbc:mysql://127.0.0.1:3306/db2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
                username: root
                password: root
            three:  #db3
                url: jdbc:mysql://127.0.0.1:3306/db3?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
                username: root
                password: root

具体实现

annotation

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {


    DataSources value() default DataSources.master;
}

 DynamicDataSourceInterceptor

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DynamicDataSourceInterceptor implements Interceptor {


    /**
     * 可以识别为 添加、更新、删除类型的 SQL 语句
     */
    public static final List<SqlCommandType> UPDATE_SQL_LIST = Arrays.asList(SqlCommandType.INSERT, SqlCommandType.UPDATE, SqlCommandType.DELETE);

    /**
     * SQL 语句中出现的悲观锁标识
     */
    private static final String LOCK_KEYWORD = "for update";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 通过 invocation 获取 MappedStatement 与 拦截方法的形参信息
        Object[] objects = invocation.getArgs();
        MappedStatement ms = (MappedStatement) objects[0];

        // 通过反射检查要执行的方法,如果标注了 @DataSource 则检查其 value
        String clazzStr = ms.getId().substring(0, ms.getId().lastIndexOf("."));
        String methodStr = ms.getId().substring(ms.getId().lastIndexOf(".") + 1);
        // 由于 mybatis 同一个接口方法不能重载
        Method[] mapperMethods = Class.forName(clazzStr).getMethods();
        Method targetMethod = null;
        for (Method mapperMethod : mapperMethods) {
            if (mapperMethod.getName().equals(methodStr)) {
                targetMethod = mapperMethod;
                break;
            }
        }
        DataSources dataSourceAnnotationValue = null;
        if (targetMethod != null && targetMethod.getAnnotation(DataSource.class) != null) {
            dataSourceAnnotationValue = targetMethod.getAnnotation(DataSource.class).value();
        }

        // 获取 sqlCommandType
        SqlCommandType sqlCommandType = ms.getSqlCommandType();

        // 获取 SQL
        BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replace("[\\t\\n\\r]", " ");

        if (dataSourceAnnotationValue == DataSources.read && sqlCommandType.equals(SqlCommandType.SELECT)) {

            DataSourceTypeManager.set(DataSources.slave);

        } else if (dataSourceAnnotationValue == DataSources.write ||
                UPDATE_SQL_LIST.contains(sqlCommandType) ||
                sql.contains(LOCK_KEYWORD)) {

            DataSourceTypeManager.set(DataSources.slave);

        } else {
            DataSourceTypeManager.set(DataSources.readmore);

        }

        Object proceed;
        try {
            proceed = invocation.proceed();
        } catch (Throwable t) {
            throw t;
        } finally {
            DataSourceTypeManager.reset();
        }
        return proceed;
    }

    @Override
    public Object plugin(Object target) {

        if (target instanceof Executor) {

            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

mybatis-config.xml

.....
    <plugins>
        <plugin interceptor="interceptor.DynamicDataSourceInterceptor" />
    </plugins>

上盘硬菜,@Transaction源码深度解析 | Spring系列第48篇 (qq.com)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是使用 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> ``` 接下来,需要实现一个继承于 MyBatisInterceptor 接口的拦截器类,该类用于在执行 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); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值