提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


文章目录

  • 前言
  • 一、配置数据源
  • 二、定义一个AbstractRoutingDataSource子类
  • 三、创建一个类管理存储访问数据库路由标识的ThreadLocal变量
  • 四、自定义注解标识当前访问路由
  • 五、AOP切面在数据库操作前获取实际使用数据库的标识
  • 六、代码结构



前言

实际项目开发过程中,有可能需要在同一个项目中接入不同数据源,Spring AbstractRoutingDataSource 提供了这方面的支持,当然要实现动态切换数据源的功能还需要另外一些步骤,我们先整体了解下

  1. 需要配置实际的数据源 AbstractRoutingDataSource 只是一个路由功能
  2. 定义一个类继承AbstractRoutingDataSource类实现determineCurrentLookupKey方法
  3. 需要有一个ThreadLocal变量,存储路由标识,表明当前应该具体使用哪个数据源
  4. 定义一个自定义注解(一般作用在dao层前面),标记当前操作的数据源
  5. 定义AOP切面在执行数据库操作前获具体使用的数据源标识(第4步注解的标识),并存入ThreadLocal线程本地变量

接下来看看具体怎么实现


一、配置数据源

  1. 配置AbstractRoutingDataSource数据源
    这里的DynamicDataSource继承了AbstractRoutingDataSource targetDataSources执行了实际的数据源
<bean id="dataSource" class="com.dlh.util.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry value-ref="dataSource1" key="db1"></entry>
                <entry value-ref="dataSource2" key="db2"></entry>
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="dataSource1" >
        </property>
    </bean>

数据源1

<bean id="dataSource1" class="com.zaxxer.hikari.HikariDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <!-- Default settings -->
        <!-- 控制自动提交行为 default:true -->
        <property name="autoCommit" value="true"/>
        <!--连接池获取的连接是否只读 default:false-->
        <property name="readOnly" value="false"/>
        <!--最大连接超时时间 default:30秒-->
        <property name="connectionTimeout" value="30000"/>
        <!--最大空闲超时时间 default:10分钟   -->
        <property name="idleTimeout" value="600000"/>
        <!--连接池中一个连接的最大生命周期 default:30分钟-->
        <property name="maxLifetime" value="1800000 "/>
    </bean>

数据源2

<bean id="dataSource2" class="com.zaxxer.hikari.HikariDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <!-- Default settings -->
        <!-- 控制自动提交行为 default:true -->
        <property name="autoCommit" value="true"/>
        <!--连接池获取的连接是否只读 default:false-->
        <property name="readOnly" value="false"/>
        <!--最大连接超时时间 default:30秒-->
        <property name="connectionTimeout" value="30000"/>
        <!--最大空闲超时时间 default:10分钟   -->
        <property name="idleTimeout" value="600000"/>
        <!--连接池中一个连接的最大生命周期 default:30分钟-->
        <property name="maxLifetime" value="1800000 "/>
    </bean>

我们用一个properties记录数据源的配置 用context:property-placeholder导入进来

<context:property-placeholder location="classpath*:jdbc.properties" />
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
jdbc.url1=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456

我们dao层用Spring来托管MyBatis,Spring整合Mybatis的配置是这样的

<!--  3. 将SqlSession对象的加载交给Spring托管  -->
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
    <!-- spring托管MyBatis,配置自动扫描数据源执行AbstractRoutingDataSource -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath*:com/dlh/dao/mapper/*.xml"></property>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.dlh.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

二、定义一个AbstractRoutingDataSource子类

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DBRoute.getDbRoute();
    }
}

这里重写determineCurrentLookupKey方法的目的是判断具体应该路由到AbstractRoutingDataSource的哪个数据源
这里需要简单了解下AbstractRoutingDataSource这个类

java 服务动态切换过滤器 filter_spring


实现了InitializingBean接口,可以在Bean初始化的时候做点事情

public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(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配置将底层的数据源注入到AbstractRoutingDataSource的targetDataSources map对象中

java 服务动态切换过滤器 filter_数据源_02

具体获取数据库连接时先判断路由到哪个数据源,这个就要子类去实现了

三、创建一个类管理存储访问数据库路由标识的ThreadLocal变量

public class DBRoute {
    private final static ThreadLocal<String> dbRoute = new ThreadLocal<>();

    public static void setDbRoute(String routeKey) {
        dbRoute.set(routeKey);
    }

    public static String getDbRoute() {
        return dbRoute.get();
    }

    public static void clearDbRoute() {
        dbRoute.remove();
    }
}

四、自定义注解标识当前访问路由

public @interface DBSwitch {
    String value() default "";
}
@DBSwitch("db1")

这个注解的值配的就是动态数据源具体数据源的Key

五、AOP切面在数据库操作前获取实际使用数据库的标识

@Aspect
@Component
public class DBSwitchAspect {

    @Pointcut("execution(* com.dlh.manager.impl.dbA.UserManagerImpl.queryAll())")
    private void dbAPointCut() {}
    @Pointcut("execution(* com.dlh.manager.impl.dbB.UserManagerImpl.queryAll())")
    private void dbBPointCut() {}
    /**
     * 前置通知:
     */
    @Before("dbAPointCut()|| dbBPointCut()")
    public void before(JoinPoint jp){
        System.out.println("前置通知 代理类"+jp.getTarget().getClass() + "代理方法 "+jp.getSignature().getName());
        Annotation[] annotations = jp.getTarget().getClass().getDeclaredAnnotations();
        DBSwitch dbSwitch = (DBSwitch) jp.getTarget().getClass().getAnnotation(DBSwitch.class);
        if(dbSwitch != null){
            DBRoute.setDbRoute( dbSwitch.value());
        }
    }
    /**
     * 后置通知
     * @param jp 连接点的基本信息
     */
    @Before("dbAPointCut()|| dbBPointCut()")
    public void after(JoinPoint jp){
        DBRoute.clearDbRoute();
    }
}

这里在执行dao层前用AOP做了拦截获取到后面实际使用数据库的标识存入线程本地变量

六、代码结构

java 服务动态切换过滤器 filter_spring_03

最后我们来运行下结果验证下

本地建了两个两个数据库来模拟多数据源

java 服务动态切换过滤器 filter_mybatis_04


java 服务动态切换过滤器 filter_数据源_05

<entry value-ref="dataSource1" key="db1"></entry>
  <entry value-ref="dataSource2" key="db2"></entry>
  1. 配置访问test1数据库
@Qualifier("userB")
    @Autowired
    private UserManager userManager;
    @Override
    public List<User> queryAll() {
        return userManager.queryAll();
    }
@Component("userB")
@DBSwitch("db2")
public class UserManagerImpl implements UserManager {
    @Autowired
    private UserDao userDao;
    @Override
    public List<User> queryAll() {
        return userDao.queryAll();
    }
}

刚好4条数据

java 服务动态切换过滤器 filter_java_06

  1. 配置访问test数据库
@Qualifier("userA")
    @Autowired
    private UserManager userManager;
    @Override
    public List<User> queryAll() {
        return userManager.queryAll();
    }
@Component("userA")
@DBSwitch("db1")
public class UserManagerImpl implements UserManager {
    @Autowired
    private UserDao userDao;
    @Override
    public List<User> queryAll() {
        return userDao.queryAll();
    }
}

刚好8条数据

java 服务动态切换过滤器 filter_数据源_07