双数据源配置一之Spring-Mybatis

参考:

因最近接触多数据源配置,在查询大量资料后,总结一些资料,供大家参考.

1 简单方式指定数据源

关于Spring多数据源的配置和使用,Spring框架预留接口,可以方便数据源的切换.

首先查看Spring获取数据源的源代码:

java 双数据源properties设置主数据源 spring双数据源_spring

可以看到AbstractRoutingDataSource获取数据源之前会先调用determineCurrentLookupKey方法查找当前的lookupKey,这个lookupKey就是数据源标识。
因此通过重写这个查找数据源标识的方法就可以让Spring切换到指定的数据源了。

第一步:创建一个DynamicDataSource的类,继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法,代码如下:

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {

        // 从自定义的位置获取数据源标识
        return DynamicDataSourceHolder.getDataSource();
    }
}

第二步:创建DynamicDataSourceHolder用于持有当前线程中使用的数据源标识,代码如下:

public class DynamicDataSourceHolder {

    /**
     * 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<String>();

    public static String getDataSource() {
        return THREAD_DATA_SOURCE.get();
    }

    public static void setDataSource(String dataSource) {
        THREAD_DATA_SOURCE.set(dataSource);
    }

    public static void clearDataSource() {
        THREAD_DATA_SOURCE.remove();
    }

}

第三步 数据准备

pom.xml

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

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.cf</groupId>
    <artifactId>ssm_demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--    当前项目编码和JDK版本-->
    <packaging>war</packaging>

    <dependencies>
        <!--springWEB-MVC-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
        <!-- aspectj支持 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.7</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.1</version>
        </dependency>

        <!--mybatis-spring-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--druid数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

        <!--jackson数据源-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!--分页插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.2</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.1</version>
                <configuration>
                    <path>/</path>
                    <port>80</port>
                    <!--解决get乱码 (tomcat7get汉字会乱码)-->
                    <uriEncoding>utf-</uriEncoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

SQL

CREATE TABLE `userinfo` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL,
  `category` varchar(255) DEFAULT NULL,
  `brand` varchar(255) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `images` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

# 数据库1数据准备
INSERT INTO `userinfo` VALUES (null, '我是JAVA工程师', '手机', '小米', 12.00, NULL);
INSERT INTO `userinfo` VALUES (null, '我是前端工程师', '手机', '小米', 24.00, NULL);
INSERT INTO `userinfo` VALUES (null, '我是后端工程师', '电脑', '华为', 36.00, NULL);

# 数据库2数据准备
INSERT INTO `userinfo` VALUES (null, '2', '3', '4', 5.00, '6');
INSERT INTO `userinfo` VALUES (null, '3', '4', '5', 6.00, '7');

jdbc.properties

# 数据源1
db1.driver=com.mysql.jdbc.Driver
db1.url=jdbc:mysql://localhost:3306/test1
db1.username=root
db1.password=root
# 数据源2
db2.driver=com.mysql.jdbc.Driver
db2.url=jdbc:mysql://localhost:3306/test2
db2.username=root
db2.password=root

spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
 
    <!--扫描基包-->
    <context:component-scan base-package="com.cf.service"/>

    <!--加载jdbc配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--整合mybatis-->
     <!--创建数据源1,连接数据库db1 -->
    <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db1.driver}"/>
        <property name="url" value="${db1.url}"/>
        <property name="username" value="${db1.username}"/>
        <property name="password" value="${db1.password}"/>
    </bean>

    <!--创建数据源2,连接数据库db2 -->
    <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db2.driver}"/>
        <property name="url" value="${db2.url}"/>
        <property name="username" value="${db2.username}"/>
        <property name="password" value="${db2.password}"/>
    </bean>
     <!--多个 -->

    <bean id="dynamicDataSource" class="com.cf.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <!-- 指定lookupKey和与之对应的数据源 -->
                <entry key="dataSource1" value-ref="dataSource1"></entry>
                <entry key="dataSource2" value-ref="dataSource2"></entry>
            </map>
        </property>

        <!--指定默认的数据源-->
        <property name="defaultTargetDataSource" ref="dataSource1"/>
    </bean>


    <!--sqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dynamicDataSource"/>
    </bean>


    <!--dao接口映射-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.cf.dao"/>
    </bean>

    <!--事务管理-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--数据源-->
        <property name="dataSource" ref="dynamicDataSource"/>
    </bean>

    <!--开启注解驱动-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

UserInfo

@Data
public class UserInfo implements Serializable {

    private String id;
    private String title;
    private String category;
    private String brand;
    private String price;
    private String images;

}

UserInfoDao

public interface UserInfoDao {

    @Select("select * from userinfo ")
    List<UserInfo> findAllUserInfo();
    
}

UserInfoService

public interface UserInfoService {

    /**
     * 查询所有用户
     * @return 用户集合
     */
    List<UserInfo> findAllUserInfo();
    List<UserInfo> findAllUserInfo1();
    List<UserInfo> findAllUserInfo2();


}

UserInfoServiceImpl

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserInfoDao userInfoDao;

    /**
     * 查询所有用户
     *
     * @return 用户集合
     */
    @Override
    public List<UserInfo> findAllUserInfo() {
        //默认数据源1
        return userInfoDao.findAllUserInfo();
    }


    @Override
    public List<UserInfo> findAllUserInfo1() {
        // 指定切换到数据源2
        DynamicDataSourceHolder.setDataSource("dataSource2");

        return userInfoDao.findAllUserInfo();
    }

    @Override
    public List<UserInfo> findAllUserInfo2() {

        // 指定切换到数据源1
        DynamicDataSourceHolder.setDataSource("dataSource1");

        return userInfoDao.findAllUserInfo();
    }

}

UserInfoController

@RestController
@RequestMapping("/userInfo")
public class UserInfoController {

    @Autowired
    private UserInfoService userInfoService;

    @RequestMapping("/findAll")
    public List<UserInfo> findAllUserInfo() {

        //查询所有用户
        List<UserInfo> userInfoList = userInfoService.findAllUserInfo();

        //打印所有用户
        userInfoList.forEach(userInfo -> System.out.println(userInfo));
        return userInfoList;
    }

    @RequestMapping("/findAll1")
    public List<UserInfo> findAllUserInfo1() {

        //查询所有用户
        List<UserInfo> userInfoList = userInfoService.findAllUserInfo1();

        //打印所有用户
        userInfoList.forEach(userInfo -> System.out.println(userInfo));

        return userInfoList;
    }

    @RequestMapping("/findAll2")
    public List<UserInfo> findAllUserInfo2() {

        //查询所有用户
        List<UserInfo> userInfoList = userInfoService.findAllUserInfo2();

        //打印所有用户
        userInfoList.forEach(userInfo -> System.out.println(userInfo));
        return userInfoList;
    }
}

第四步 结果展示

默认数据源(数据源1):

java 双数据源properties设置主数据源 spring双数据源_xml_02

数据源2:

java 双数据源properties设置主数据源 spring双数据源_数据源_03

数据源1:

java 双数据源properties设置主数据源 spring双数据源_mybatis_04

问题: 每次切换数据源都需要调用重写的DynamicDataSourceHolder.setDataSource() ,代码之前十分繁琐,后期维护困难,能否通过注解方式指定需要的数据源呢?

通过自定义注解,定义AOP切面以便拦截所有带有注解@DataSource的方法,取出注解的值作为数据源标识放到DynamicDataSourceHolder的线程变量中,来实现切换数据源.

2 使用注解方式指定数据源

第一步 自定义一个名为DataSource的注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    String value();
}

第二步 定义AOP切面方法

public class DataSourceAspect {

    /**
     * 拦截目标方法,获取由@DataSource指定的数据源标识,设置到线程存储中以便切换数据源
     *
     * @param point
     * @throws Exception
     */
    public void intercept(JoinPoint point) throws Exception {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 默认使用目标类型的注解,如果没有则使用其实现接口的注解
        for (Class<?> clazz : target.getInterfaces()) {
            resolveDataSource(clazz, signature.getMethod());
        }
        resolveDataSource(target, signature.getMethod());
    }

    /**
     * 提取目标对象方法注解和类型注解中的数据源标识
     *
     * @param clazz
     * @param method
     */
    private void resolveDataSource(Class<?> clazz, Method method) {
        try {
            Class<?>[] types = method.getParameterTypes();
            // 默认使用类型注解
            if (clazz.isAnnotationPresent(DataSource.class)) {
                DataSource source = clazz.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.setDataSource(source.value());
            }
            // 方法注解可以覆盖类型注解
            Method m = clazz.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                DataSource source = m.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.setDataSource(source.value());
            }
        } catch (Exception e) {
            System.out.println(clazz + ":" + e.getMessage());
        }
    }

}

第三步 在配置文件中配置拦截规则

在上面spring.xml中添加

<bean id="dataSourceAspect" class="com.cf.aspect.DataSourceAspect"/>

    <aop:config>
        <aop:aspect ref="dataSourceAspect">
            <!--拦截所有service方法-->
            <aop:pointcut id="dataSourcePointcut" expression="execution(* com.cf.service.impl.*.*(..))"/>
            <aop:before method="intercept" pointcut-ref="dataSourcePointcut"/>
        </aop:aspect>
    </aop:config>

第四步 使用注解来指定数据源

注解使用优先级: 方法>实现类>接口 (三者同时存在,以方法上为准)

UserInfoServiceImpl

@Service
@DataSource("dataSource1")
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserInfoDao userInfoDao;

	//使用类上数据源
    @Override
    public List<UserInfo> findAllUserInfo() {
        
        return userInfoDao.findAllUserInfo();
    }

	//类上,方法上同时存在注解,优先使用方法上数据源
    @Override
    @DataSource("dataSource2")
    public List<UserInfo> findAllUserInfo1() {
  
        return userInfoDao.findAllUserInfo();
    }

    //类上,方法上同时存在注解,优先使用方法上数据源
    @Override
    @DataSource("dataSource1")
    public List<UserInfo> findAllUserInfo2() {

        return userInfoDao.findAllUserInfo();
    }

}

第五步 结果展示

类上数据源(数据源1):

java 双数据源properties设置主数据源 spring双数据源_mybatis_05


方法上数据源(数据源2):

java 双数据源properties设置主数据源 spring双数据源_spring_06


方法上数据源(数据源1):

java 双数据源properties设置主数据源 spring双数据源_xml_07

项目完整结构图:

java 双数据源properties设置主数据源 spring双数据源_mybatis_08