用户登录
用户注册

分享至

通过AOP实现MyBatis多数据源的动态切换实例教程

  • 作者: 守护么么哒
  • 来源: 51数据库
  • 2021-10-15

在后面的工作中,有一个项目也需要用到多,但是是在一个方法中,里面涉及到两个查询,可能还要和线程进行绑定。这就涉及到在查询的时候切换数据库。这个文章写的也很不错。现在分享给大家。

【环境参数】

1.开发框架:spring + springmvc + mybatis

2.数据库a的url:jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterencoding=utf-8&useunicode=true&autoreconnect=true&failoverreadonly=false

3.数据库b的url:bakdb.jdbc.url=jdbc:mysql://172.16.17.68:3306/bakdb?characterencoding=utf-8&useunicode=true&autoreconnect=true&failoverreadonly=false

【需求描述】

(1)当用户调用x方法“之前”,会首先切换当前数据源为a数据源(bakdb数据库),之后再去调用方法x。

(2)当用户调用y方法“之前”,系统会首先切换当前的数据源为b数据源(testdb数据库),之后再去调用方法y。

(3)x方法和y方法所在的包名

    3.1) x方法:该方法位于com.zjrodger.bakdata.service包下其子包下。

    3.2) y方法:该方法位于com.zjrodger.datatobank.service或者com.zjrodger.zxtobank.service包及其子包下。

【具体步骤】

1、编写动态数据源相关代码。

(1) 编写dynamicdatasource类。

dynamicdatasource的主要作用是以map的形式,来存储多个数据源。

因为该类继承了父类abstractroutingdatasource,在父类中,多数据源的实例是被存放在一个名为“targetdatasource”的map类型的成员变量中。

点击(此处)折叠或打开

import org.springframework.jdbc.datasource.lookup.abstractroutingdatasource;

public class dynamicdatasource extends abstractroutingdatasource {

    @override

    protected object determinecurrentlookupkey() {

        return databasecontextholder.getdbtype();

    }

}

(2) 编写databasecontextholder类。

点击(此处)折叠或打开

public class databasecontextholder {

    private static final threadlocal<string> contextholder = new threadlocal<string>();

    public static void setdbtype(string datasourcetype) {

        contextholder.set(datasourcetype);

    }

    public static string getdbtype() {

        return contextholder.get();

    }

    public static void cleardbtype() {

        contextholder.remove();

    }

}

2、编写切换数据源的拦截器。

点击(此处)折叠或打开

public class datasourceinterceptor {

    

    /** 数据源切换常量 */

    public static final string datasource_test_db="datasourcekey4testdb";

    public static final string datasource_bak_db="datasourcekey4bakdb";

    /**

     * 设置数据源为test数据库所对应的数据源。

     * @param jp

     */

    public void setdatasourcetestdb(joinpoint jp) {

        databasecontextholder.setdbtype(datasource_test_db);

    }

    

    /**

     * 设置数据源为bak数据库所对应的数据源。

     * @param jp

     */

    public void setdatasourcebakdb(joinpoint jp) {

        databasecontextholder.setdbtype(datasource_bak_db);

    }

}

3、在spring配置文件中进行相关配置。

(1)配置两个数据源

a.第一个数据源:

点击(此处)折叠或打开

<bean id="c3p0datasource4bakdb" class="com.mchange.v2.c3p0.combopooleddatasource"

    destroy-method="close" depends-on="propertyconfigurer">

    <property name="driverclass" value="${bakdb.jdbc.driverclass}" />

    <property name="jdbcurl" value="${bakdb.jdbc.url}" />

    <property name="user" value="${bakdb.jdbc.username}" />

    <property name="password" value="${bakdb.jdbc.password}" />

    <!-- 初始化时获取的连接数,取值应在minpoolsize与maxpoolsize之间。default: 3 -->

    <property name="initialpoolsize" value="10" />

    <!-- 连接池中保留的最小连接数。 -->

    <property name="minpoolsize" value="5" />

    <!-- 连接池中保留的最大连接数。default: 15 -->

    <property name="maxpoolsize" value="100" />

    <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。default: 3 -->

    <property name="acquireincrement" value="5" />

    <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。default: 0 -->

    <property name="maxidletime" value="10" />

    <!-- jdbc的标准参数,用以控制数据源内加载的preparedstatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 

        如果maxstatements与maxstatementsperconnection均为0,则缓存被关闭。default: 0 -->

    <property name="maxstatements" value="0" />

    <!-- 连接池用完时客户调用getconnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 sqlexception,如果设置0,则无限等待。default:0 -->

    <property name="checkouttimeout" value="30000" />

</bean>

b.第二个数据源:

点击(此处)折叠或打开

<bean id="c3p0datasource4testdb" class="com.mchange.v2.c3p0.combopooleddatasource"

    destroy-method="close" depends-on="propertyconfigurer">

    <property name="driverclass" value="${jdbc.driverclass}" />

    <property name="jdbcurl" value="${jdbc.url}" />

    <property name="user" value="${jdbc.username}" />

    <property name="password" value="${jdbc.password}" />

    <!-- 初始化时获取的连接数,取值应在minpoolsize与maxpoolsize之间。default: 3 -->

    <property name="initialpoolsize" value="10" />

    <!-- 连接池中保留的最小连接数。 -->

    <property name="minpoolsize" value="5" />

    <!-- 连接池中保留的最大连接数。default: 15 -->

    <property name="maxpoolsize" value="100" />

    <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。default: 3 -->

    <property name="acquireincrement" value="5" />

    <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。default: 0 -->

    <property name="maxidletime" value="10" />

    <!-- jdbc的标准参数,用以控制数据源内加载的preparedstatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 

        如果maxstatements与maxstatementsperconnection均为0,则缓存被关闭。default: 0 -->

    <property name="maxstatements" value="0" />

    <!-- 连接池用完时客户调用getconnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 sqlexception,如果设置0,则无限等待。default:0 -->

    <property name="checkouttimeout" value="30000" />

</bean>

(2)两个数据源所对应的properties属性文件

点击(此处)折叠或打开

# =========== test数据库相关信息 ============

jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterencoding=utf-8&amp;useunicode=true&amp;autoreconnect=true&amp;failoverreadonly=false

jdbc.username=root

jdbc.password=123456

jdbc.driverclass=com.mysql.jdbc.driver

jdbc.ip=172.16.5.64

jdbc.dbname=test

# =========== bakdb数据库相关信息 ============

bakdb.jdbc.url=jdbc:mysql://172.16.17.68:3306/bakdb?characterencoding=utf-8&amp;useunicode=true&amp;autoreconnect=true&amp;failoverreadonly=false

bakdb.jdbc.username=root

bakdb.jdbc.password=123456

bakdb.jdbc.driverclass=com.mysql.jdbc.driver

bakdb.jdbc.ip=172.16.17.68

bakdb.jdbc.dbname=bakdb

(3)配置dynamicdatasource这个bean(关键)。

该dynamicdatasource的主要作用是以map的形式,来存储多个数据源。

点击(此处)折叠或打开

<!-- 配置可以存储多个数据源的bean -->

<bean id="datasource" class="com.beebank.pub.datasource.dynamicdatasource">

    <property name="targetdatasources">

        <map key-type="java.lang.string">

            <entry key="datasourcekey4testdb" value-ref="c3p0datasource4testdb" />

            <entry key="datasourcekey4bakdb" value-ref="c3p0datasource4bakdb" />

        </map>

    </property>

    <property name="defaulttargetdatasource" ref="c3p0datasource4huihangdb" />

</bean> 

配置datasource这个bean

(4)配置datasourceinterceptor这个bean(关键)。

点击(此处)折叠或打开

1 <!-- 配置切换数据源key的拦截器 -->

2 <bean id="datasourceinterceptor" class="com.zjrodger.pub.datasource.datasourceinterceptor"/>

(5)利用aop,配置控制数据源在特定条件下切换的切面(关键,重要)。

注意要添加aop名字空间。

配置spring事务切面和自定义切面类,动态切换数据源,注意两切面的执行顺序。

点击(此处)折叠或打开

<!-- 1.配置spring框架自身提供的切面类 -->

<tx:advice id="usertxadvice" transaction-manager="transactionmanager">

    <tx:attributes>

        <tx:method name="delete*" propagation="required" read-only="false"

            rollback-for="java.lang.exception" no-rollback-for="java.lang.runtimeexception" />

        <tx:method name="insert*" propagation="required" read-only="false"

            rollback-for="java.lang.exception" />

        <tx:method name="update*" propagation="required" read-only="false"

            rollback-for="java.lang.exception" />

        <tx:method name="find*" propagation="supports" />

        <tx:method name="get*" propagation="supports" />

        <tx:method name="select*" propagation="supports" />

    </tx:attributes>

</tx:advice>

<!-- 2.配置用户自定义的切面,用于切换数据源key -->

<bean id="datasourceinterceptor" class="com.zjrodger.pub.datasource.datasourceinterceptor"></bean> 

<!-- 3.(重要)配置spring事务切面和自定义切面类,动态切换数据源,注意两切面的执行顺序 -->

<aop:config> 

    <!-- (1) spring框架自身提供的切面 -->

    <aop:advisor advice-ref="usertxadvice" pointcut="execution(public * com.zjrodger.*.service..*.*(..))" order="2"/> 

     

    <!-- (2) 用户自定义的切面,根据切入点,动态切换数据源。 --> 

    <aop:ect id="datasourceaspect" ref="datasourceinterceptor" order="1"> 

        <aop:before method="setdatasourcebakdb" pointcut="execution(* com.zjrodger.bakdata.service..*.*(..))"/> 

        <aop:before method="setdatasourcetestdb" pointcut="execution(* com.zjrodger.datatobank.service..*.*(..))"/>

    </aop:aspect> 

</aop:config>

5.1) 注意:

a.注意上述两个切面中的order属性的配置。

b.自定义切面类和spring自带事务切面类(即元素)的执行的先后顺序要配置正确,否则就会导致导致数据源不能动态切换。

    在aop中,当执行同一个切入点时,不同切面的执行先后顺序是由“每个切面的order属性”而定的,order越小,则该该切面中的通知越先被执行。

上述元素中,引用了两个切面类:“usertxadvice类”和“datasourceaspect类”,其中是spring框架自定义的切面标签。

根据两个切面类order属性的定义,当程序执行时并且触发切入点后(即调用com.zjrodger.bakdata.service包及其子包下的方法),datasourceaspect切面类中的setdatasourcebakdb()方通知法首先执行,之后才会执行usertxadvice事务类中的相关通知方。

5.2) 配置文件作用说明

切面类“datasourceinterceptor”中有两个方法:setdatasourcetestdb()方法和setdatasourcebakdb()。

1)当用户调用“com.zjrodger.bakdata.service”包及其子包下的方法x“之前”,系统会首先去调用setdatasourcebakdb()方法,设置当前数据源为bakdb的数据源,之后再去调用方法x。

2)当用户调用“com.zjrodger.datatobank.service”或者“com.zjrodger.zxtobank.service”包及其子包下的方法y之前,系统会首先去调调用setdatasourcetestdb()方法,设置当前的数据源为testdb数据库的数据源,之后再去调用方法y。

(6)完整的spring配置文档

点击(此处)折叠或打开

<?xml version="1.0" encoding="utf-8"?>

<beans xmlns="http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017478.jpg"

    xmlns:xsi="https://www.w3.org/2001/xmlschema-instance" xmlns:context="http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017493.jpg"

    xmlns:tx="http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161016009.jpg" xmlns:aop="http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017509.jpg"

    xmlns:p="https://www.springframework.org/schema/p" xmlns:m="http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017462.jpg"

    xmlns:task="http://www.51sjk.com/Upload/Articles/1/0/301/301069_2021072816101746201.jpg"

    xsi:schemalocation="http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017462.jpg http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017462.jpg/spring-mvc.xsd

        http://www.51sjk.com/Upload/Articles/1/0/301/301069_2021072816101746201.jpg http://www.51sjk.com/Upload/Articles/1/0/301/301069_2021072816101746201.jpg/spring-task.xsd

        http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017478.jpg http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017478.jpg/spring-beans.xsd

        http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017493.jpg http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017493.jpg/spring-context.xsd

        http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017509.jpg http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017509.jpg/spring-aop.xsd

        http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017524.jpg http://www.51sjk.com/Upload/Articles/1/0/301/301069_20210728161017524.jpg/spring-tx.xsd">

    <!-- 该配置为自动扫描配置的包下所有使用@controller注解的类 -->

    <context:component-scan base-package="com.zjrodger" />

    <bean id="propertyconfigurer"

        class="org.springframework.beans.factory.config.propertyplaceholderconfigurer">

        <property name="location">

            <value>classpath:properties/dbconfig.properties</value>

        </property>

        <property name="fileencoding" value="utf-8" />

    </bean>

    

    <!-- 备份库数据库数据源 -->

    <bean id="c3p0datasource4bakdb" class="com.mchange.v2.c3p0.combopooleddatasource"

        destroy-method="close" depends-on="propertyconfigurer">

        <property name="driverclass" value="${bakdb.jdbc.driverclass}" />

        <property name="jdbcurl" value="${bakdb.jdbc.url}" />

        <property name="user" value="${bakdb.jdbc.username}" />

        <property name="password" value="${bakdb.jdbc.password}" />

        <!-- 初始化时获取的连接数,取值应在minpoolsize与maxpoolsize之间。default: 3 -->

        <property name="initialpoolsize" value="10" />

        <!-- 连接池中保留的最小连接数。 -->

        <property name="minpoolsize" value="5" />

        <!-- 连接池中保留的最大连接数。default: 15 -->

        <property name="maxpoolsize" value="100" />

        <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。default: 3 -->

        <property name="acquireincrement" value="5" />

        <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。default: 0 -->

        <property name="maxidletime" value="10" />

        <!-- jdbc的标准参数,用以控制数据源内加载的preparedstatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 

            如果maxstatements与maxstatementsperconnection均为0,则缓存被关闭。default: 0 -->

        <property name="maxstatements" value="0" />

        <!-- 连接池用完时客户调用getconnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 sqlexception,如果设置0,则无限等待。default:0 -->

        <property name="checkouttimeout" value="30000" />

    </bean>

    

    <!--test数据库数据源 -->

    <bean id="c3p0datasource4testdb" class="com.mchange.v2.c3p0.combopooleddatasource"

        destroy-method="close" depends-on="propertyconfigurer">

        <property name="driverclass" value="${jdbc.driverclass}" />

        <property name="jdbcurl" value="${jdbc.url}" />

        <property name="user" value="${jdbc.username}" />

        <property name="password" value="${jdbc.password}" />

        <!-- 初始化时获取的连接数,取值应在minpoolsize与maxpoolsize之间。default: 3 -->

        <property name="initialpoolsize" value="10" />

        <!-- 连接池中保留的最小连接数。 -->

        <property name="minpoolsize" value="5" />

        <!-- 连接池中保留的最大连接数。default: 15 -->

        <property name="maxpoolsize" value="100" />

        <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。default: 3 -->

        <property name="acquireincrement" value="5" />

        <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。default: 0 -->

        <property name="maxidletime" value="10" />

        <!-- jdbc的标准参数,用以控制数据源内加载的preparedstatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 

            如果maxstatements与maxstatementsperconnection均为0,则缓存被关闭。default: 0 -->

        <property name="maxstatements" value="0" />

        <!-- 连接池用完时客户调用getconnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 sqlexception,如果设置0,则无限等待。default:0 -->

        <property name="checkouttimeout" value="30000" />

    </bean>

    

    <!-- 配置可以存储多个数据源的bean -->

    <bean id="datasource" class="com.zjrodger.pub.datasource.dynamicdatasource">

        <property name="targetdatasources">

            <map key-type="java.lang.string">

                <entry key="datasourcekey4testdb" value-ref="c3p0datasource4testdb" />

                <entry key="datasourcekey4bakdb" value-ref="c3p0datasource4bakdb" />

            </map>

        </property>

        <property name="defaulttargetdatasource" ref="c3p0datasource4testdb" />

    </bean> 

    <bean id="sqlsessionfactory" class="org.mybatis.spring.sqlsessionfactorybean">

        <!-- <property name="datasource" ref="c3p0datasource" /> -->

        <property name="datasource" ref="datasource" />

        <property name="mapperlocations" value="classpath*:com/zjrodger/**/dao/xml/*.xml" />

        <!-- 添加分页插件 -->

        <property name="plugins">

            <list>

                <bean class="com.github.pagehelper.pagehelper">

                    <property name="properties">

                        <props>

                            <prop key="dialect">mysql</prop>

                            <prop key="offsetaspagenum">true</prop>

                            <prop key="rowboundswithcount">true</prop>

                            <prop key="pagesizezero">true</prop>

                            <prop key="reasonable">true</prop>

                        </props>

                    </property>

                </bean>

            </list>

        </property>

    </bean>

    <bean id="transactionmanager"

        class="org.springframework.jdbc.datasource.datasourcetransactionmanager">

        <!-- <property name="datasource" ref="c3p0datasource" /> -->

        <property name="datasource" ref="datasource" />

    </bean>

    <!-- 注解驱动,使spring的controller全部生效 -->

    <mvc:annotation-driven />

    <!-- 注解驱动,是spring的task全部生效 -->

    <task:annotation-driven />

    <aop:aspectj-autoproxy expose-proxy="true" /> 

    <tx:annotation-driven transaction-manager="transactionmanager"/> 

    <!-- spring声明式事务切面 -->

    <tx:advice id="usertxadvice" transaction-manager="transactionmanager">

        <tx:attributes>

            <tx:method name="delete*" propagation="required" read-only="false"

                rollback-for="java.lang.exception" no-rollback-for="java.lang.runtimeexception"/>

            <tx:method name="insert*" propagation="required" read-only="false"

                rollback-for="java.lang.exception" />

            <tx:method name="update*" propagation="required" read-only="false"

                rollback-for="java.lang.exception" />

            <tx:method name="find*" propagation="supports" />

            <tx:method name="get*" propagation="supports" />

            <tx:method name="select*" propagation="supports" />

        </tx:attributes>

    </tx:advice>

    <aop:config> 

        <!-- spring框架自身提供的切面 -->

        <aop:advisor advice-ref="usertxadvice" pointcut="execution(public * com.zjrodger.*.service..*.*(..))" order="2"/> 

    

        <!-- 用户自定义的切面,根据切入点,动态切换数据源。 --> 

        <aop:aspect id="datasourceaspect" ref="datasourceinterceptor" order="1"> 

            <aop:before method="setdatasourcebakdb" pointcut="execution(* com.zjrodger.bakdata.service..*.*(..))"/> 

            <aop:before method="setdatasourcetestdb" pointcut="execution(* com.zjrodger.datatobank.service..*.*(..))"/>

            <aop:before method="setdatasourcetestdb" pointcut="execution(* com.zjrodger.zxtobank.service..*.*(..))"/> 

        </aop:aspect> 

    </aop:config>

    

    <!-- 配置切换数据源key的拦截器 -->

    <bean id="datasourceinterceptor" class="com.zjrodger.pub.datasource.datasourceinterceptor"></bean> 

    

    <!-- mybatis配置 -->

    <bean class="org.mybatis.spring.mapper.mapperscannerconfigurer">

        <property name="basepackage" value="com.zjrodger.pub.dao,com.zjrodger.zxtobank.dao,com.zjrodger.bakdata.dao" />

        <property name="sqlsessionfactorybeanname" value="sqlsessionfactory" />

    </bean> 

</beans>

至此,mybatis多数据源的配置完毕,之后在自己的环境下进行测试,结果测试通过。

要特别注意自定义aop切面与spring自带的事务切面的执行顺序,即注意中的配置部分,否则,很容易会出现动态切换数据源失败的现象。

软件
前端设计
程序设计
Java相关