SpringBoot+MyBatis 动态数据源(内附项目地址)

项目结构
项目结构

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.avanty</groupId>
    <artifactId>dds</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>dds</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>

        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>2.7.8</version>
        </dependency>

    </dependencies>

    <!--创建可执行jar-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

application.yml

---
spring:
  datasource:
      master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            type: com.zaxxer.hikari.HikariDataSource
            jdbcUrl: jdbc:mysql://127.0.0.1:3306/DB1?characterEncoding=utf-8&useSSL=false
            username: root
            password: ******
      slave:
            driver-class-name: com.mysql.cj.jdbc.Driver
            type: com.zaxxer.hikari.HikariDataSource
            jdbcUrl: jdbc:mysql://127.0.0.1:3306/DB2?characterEncoding=utf-8&useSSL=false
            username: root
            password: ******

mybatis:
    type-aliases-package: com.avanty.mapper
    mapper-locations: classpath:mapper/*Mapper.xml

User1Mapper.xml
这里主要是为了验证动态数据源的切换,所以mapper里面就写一个查询
namespace是*Mapper.xml的接口类(DAO层),类名与id相同,resultType是实体类

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.avanty.mapper.User1Mapper">
    <select id="find1ByName" resultType="com.avanty.pojo.User1">
        SELECT * FROM User1 WHERE username=#{username}
    </select>
</mapper>

User1实体类

package com.avanty.pojo;

import java.io.Serializable;
import java.sql.Timestamp;

/**
 * @author zhan
 */

public class User1 implements Serializable {

    private int id;
    private String username;
    private int age;
    private Timestamp create_time;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUserName() {
        return username;
    }

    public void setUserName(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Timestamp getCreate_time() {
        return create_time;
    }

    public void setCreate_time(Timestamp create_time) {
        this.create_time = create_time;
    }

    @Override
    public String toString() {
        return "User1{" +
                "id=" + id +
                ", name='" + username + '\'' +
                ", age=" + age +
                ", create_time=" + create_time +
                '}';
    }
}

service实现DAO层接口

package com.avanty.service;

import com.avanty.mapper.User1Mapper;
import com.avanty.pojo.User1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author zhan
 */
@Service(value = "User1Mapper")
public class User1ServiceImpl implements User1Mapper {

    @Autowired
    User1Mapper user1;

    @Override
    public User1 find1ByName(String name) {
        return user1.find1ByName(name);
    }
}

最后引入动态数据源的Java配置文件:
DynamicRoutingDataSource.java

package com.avanty.dds;

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

/**
 * @author zhan
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

DynamicDataSourceAspect.java
作用:是在数据操作之前切换数据源,完成操作后将数据源重置为默认数据源

package com.avanty.dds;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @author zhan
 */
@Aspect
// 该切面应当先于 @Transactional 执行
@Order(-1)
@Component
public class DynamicDataSourceAspect {
    /** * Switch DataSource * * @param point * @param targetDataSource */
    @Before("@annotation(targetDataSource))")
    public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) {
        if (!DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value())) {
            System.out.println("DataSource [{}] doesn't exist, use default DataSource [{}] " + targetDataSource.value());
        } else {
            // 切换数据源
            DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value());
            System.out.println("Switch DataSource to [{}] in Method [{}] " +
                    DynamicDataSourceContextHolder.getDataSourceKey() + point.getSignature());
        }
    }

    /** * Restore DataSource * * @param point * @param targetDataSource */
    @After("@annotation(targetDataSource))")
    public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) {
        // 将数据源置为默认数据源
        DynamicDataSourceContextHolder.clearDataSourceKey();
        System.out.println("Restore DataSource to [{}] in Method [{}] " +
                DynamicDataSourceContextHolder.getDataSourceKey() + point.getSignature());
    }
}

DynamicDataSourceContextHolder.java

package com.avanty.dds;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhan
 */

public class DynamicDataSourceContextHolder {

    /** * Maintain variable for every thread, to avoid effect other thread */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {

        /** * 将 master 数据源的 key 作为默认数据源的 key */
        @Override
        protected String initialValue() {
            return "master";
        }
    };


    /** * 数据源的 key 集合,用于切换时判断数据源是否存在 */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /** * To switch DataSource * * @param key the key */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /** * Get current DataSource * * @return data source key */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /** * To set DataSource as default */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }

    /** * Check if give DataSource is in current DataSource list * * @param key the key * @return boolean boolean */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }
}

TargetDataSource.java
作用:数据源切换接口

package com.avanty.dds;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String value();
}

DataSourceConfigurer.java
这部分为重点,首先@MapperScan扫描DAO层接口类,随后读取数据源配置,将多数据源以键值对形式放入Map中,方便后续通过命名切换数据源,最后两种方法都能对应到不同的数据源

package com.avanty.dds;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

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

/**
 * @author zhan
 */
@Configuration
@MapperScan(basePackages = {"com.avanty.mapper"})
public class DataSourceConfigurer {

    /**
     * master DataSource
     *
     * @return data source
     * @Primary 注解用于标识默认使用的 DataSource Bean,因为有多个 DataSource Bean,该注解可用于 master
     * 或 slave DataSource Bean, 但不能用于 dynamicDataSource Bean, 否则会产生循环调用
     * @ConfigurationProperties 注解用于从 application.properties 或 application.yml 文件中读取配置,为 Bean 设置属性
     */
    @Bean("master")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource master() {
        return DataSourceBuilder.create().build();
    }

    /**
     * slave DataSource * * @return data source
     */
    @Bean("slave")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slave() {
        return DataSourceBuilder.create().build();
    }

    /**
     * Dynamic data source. * * @return the data source
     */
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("master", master());
        dataSourceMap.put("slave", slave());
        // 将 master 数据源作为默认指定的数据源
        dynamicRoutingDataSource.setDefaultTargetDataSource(master());
        // 将 master 和 slave 数据源作为指定的数据源
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);

        // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
        DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
        return dynamicRoutingDataSource;
    }

    /**
     * 配置 SqlSessionFactoryBean
     *
     * @return the sql session factory bean
     * @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到
     *
     * @ConfigurationProperties(prefix = "mybatis") 不写,Reason: No converter found capable of converting from type [java.lang.String] to type [@org.springframework.boot.context.properties.ConfigurationProperties org.mybatis.spring.SqlSessionFactoryBean]
     * Github 资料表明与 SpringBoot 2.0 有关
     */

    //way 1:
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
        return sqlSessionFactoryBean;
    }


    //way 2:
    /*
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        //此处设置为了解决找不到mapper文件的问题
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory());
    }
    */

    /**
     * 配置事务管理,如果使用到事务需要注入该 Bean,否则事务不会生效
     * 在需要的地方加上 @Transactional 注解即可
     *
     * @return the platform transaction manager
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

GitHub上原作者只写了方法一,而且是这么解释的,这个坑踩了很久。他说没有xml可以不配置,但是我有xml(并且放在resource目录下,项目编译后也存在xml),可是这么配置一直报错无法创建Bean,原因是不存在xml。这里可能是因为SpringBoot2.0的原因,所以跟之前版本的配置不一样了。

/**
     * 配置 SqlSessionFactoryBean
     *
     * @return the sql session factory bean
     * @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到
     * Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
     *
     */

    @Bean
    @ConfigurationProperties(prefix = "mybatis")
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        return sqlSessionFactoryBean;
    }

最后写个Controller,用@TargetDataSource(value = “XXX”)切换数据源
Hello.java

package com.avanty.controller;

import com.avanty.dds.TargetDataSource;
import com.avanty.pojo.User1;
import com.avanty.pojo.User2;
import com.avanty.service.User1ServiceImpl;
import com.avanty.service.User2ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zhan
 */

@RestController
public class Hello {

    @Autowired
    User1ServiceImpl user1ServiceImpl;

    @Autowired
    User2ServiceImpl user2ServiceImpl;

    @RequestMapping("/hello")
    public String hello(){
        return "Hello World!";
    }

    @GetMapping("/user1")
    @TargetDataSource(value = "master")
    public User1 user1Select(String name){
        return user1ServiceImpl.find1ByName(name);
    }

    @GetMapping("/user2")
    @TargetDataSource(value = "slave")
    public User2 user2Select(String name){
        return user2ServiceImpl.find2ByName(name);
    }

}

之所以卡了好几天还是没有放弃这个解决方案的原因可能是比较灵活吧,不需要写太多Configuration
项目地址(基于maven):
https://gitee.com/ichampion/SpringBoot-MyBatis-DynamicDataSource.git
原作者项目地址(基于gradle):
https://github.com/helloworlde/SpringBoot-DynamicDataSource
现在动态数据源会了,再看一下 CentOS7中MySQL主从配置的一些收获 学会主从配置啊,甚至集群啊,是不是就可以会读写分离了?
最后记两个注解,既然开始玩后端了,就要多学习多记
注解

猜你喜欢

转载自blog.csdn.net/u012138272/article/details/79499358