springBoot-mybatis+druid多数据源

Posted 野生java研究僧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springBoot-mybatis+druid多数据源相关的知识,希望对你有一定的参考价值。

springBoot多数据源

配置多数据源有多种方式,这里使用的是AOP动态代理的方式进行动态切换的。所谓的动态数据源就是,多个数据源,可以进行切换,比如读写分离,读是一个数据源,写又是一个数据源。不过使用这种方式配置的动态数据源不适合动态扩展,当需要新增数据源的时候,只能重启服务进行重新配置,当然也可以使用nacos配置中新做动态刷新,我只是想到可以怎么做,但是具体没有做过。

AbstractRoutingDataSource

动态数据源最核心的就是AbstractRoutingDataSource 这个抽象类,我们只需要继承这个抽象类,然后重写 determineCurrentLookupKey()方法给他返回一个数据源名称就可以了,到时候他会根据你返回的数据源名称去resolvedDataSources这个map集合中找,resolvedDataSources这个map集合的key就是数据源名称,value对应的就是具体的数据库连接。

    @Nullable
    protected abstract Object determineCurrentLookupKey();

这个是父类的方法,我们在子类进行重写。到时候父类会调用determineTargetDataSource()方法确定目标数据源,然后就调用到子类的实现,这也是人家留给我们的一个扩展,让我能动态的确定数据源。

    protected DataSource determineTargetDataSource() 
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) 
            dataSource = this.resolvedDefaultDataSource;
        

        if (dataSource == null) 
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
         else 
            return dataSource;
        
    

引入依赖

<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>8.0.18</version>
		</dependency>
        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.3.0</version>
        </dependency>

    </dependencies>

配置文件

spring:
  datasource:
      master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            type: com.alibaba.druid.pool.DruidDataSource
            url: jdbc:mysql://127.0.0.1:3306/db1?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
            username: root
            password: root
      slave:
            driver-class-name: com.mysql.cj.jdbc.Driver
            type: com.alibaba.druid.pool.DruidDataSource
            url: jdbc:mysql://122.132.222.110:3306/db2?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=GMT%2B8
            username: root
            password: root

mybatis:
    type-aliases-package: com.compass.mapper
    mapper-locations: classpath:mapper/*.xml
server:
  port: 8080

动态数据源配置

1.定义标识使用那个数据源的注解

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


2.数据源配置类

import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
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;
import java.util.Properties;


@Configuration
@MapperScan(basePackages = "com.compass.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 new DruidDataSource();
    

    /**
     * slave DataSource * * @return data source
     */
    @Bean("slave")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slave() 
      return new DruidDataSource();
    

    /**
     * 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 位置和持久层接口的别名设置到
     * Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
     *
     * @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 有关
     */

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

        // 配置分页插件
        setPagePlugins(sqlSessionFactoryBean);

        // 配置mapper映射路径
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
        return sqlSessionFactoryBean;
    


    /**
     * 方式2:
     * @return
     * @throws Exception
     */
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception 
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        sqlSessionFactoryBean.setDataSource(dynamicDataSource());

        // 配置分页插件
        setPagePlugins(sqlSessionFactoryBean);

        // 配置mapper映射路径
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
        return sqlSessionFactoryBean.getObject();
    

    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws Exception 
        return new SqlSessionTemplate(sqlSessionFactory());
    

    /**
     * 分页插件
     * @param sqlSessionFactoryBean
     */
    public void  setPagePlugins(SqlSessionFactoryBean sqlSessionFactoryBean)
        Properties properties = new Properties();
        properties.setProperty("reasonable", "true");
        properties.setProperty("supportMethodsArguments", "true");
        properties.setProperty("pageSizeZero", "true");

        PageInterceptor interceptor = new PageInterceptor();
        interceptor.setProperties(properties);

        sqlSessionFactoryBean.setPlugins(new Interceptor[]interceptor);
    


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

3.控制动态数据源的AOP切面

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;


@Aspect
// 该切面应当先于 @Transactional 执行
@Order(-1)
@Component
public class DynamicDataSourceAspect 
    /** * Switch DataSource * * @param point * @param targetDataSource */
    // @annotation:用来拦截所有被某个注解修饰的方法
    @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());
    

4.动态数据源的封装

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


public class DynamicDataSourceContextHolder 

    /** 为每个线程维护变量,以避免影响其他线程 */
    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<>();

    /** *要切换 DataSource @param key 键 */
    public static void setDataSourceKey(String key) 
        contextHolder.set(key);
    

    /** * 获取当前DataSource @return 数据源key */
    public static String getDataSourceKey() 
        return contextHolder.get();
    

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

    /** * 检查给定数据源是否在当前数据源列表中 @param  key @return boolean */
    public static boolean containDataSourceKey(String key) 
        return dataSourceKeys.contains(key);
    

5.继承AbstractRoutingDataSource返回确定的数据源

public class DynamicRoutingDataSource extends AbstractRoutingDataSource 
    @Override
    protected Object determineCurrentLookupKey() 
        return DynamicDataSourceContextHolder.getDataSourceKey();
    


基本上配置就完毕了。

大致思路,不是很详细,大家可以自行debug

  1. 在 this.finishBeanFactoryInitialization(beanFactory);刷新工厂初始化单实例bean的时候就会初始化SqlSessionFactoryBean,在初始化SqlSessionFactoryBean的途中去初始化DataSource,给AbstractRoutingDataSource的targetDataSources属性赋值,将我们配置的数据源全部都给他。给AbstractRoutingDataSource的defaultTargetDataSource属性也赋值,指定我们默认的数据源。
  2. 在AbstractRoutingDataSource的afterPropertiesSet()方法中将targetDataSources中的数据源赋值给resolvedDataSources,最后将defaultTargetDataSource的值赋值给resolvedDefaultDataSource
  3. SqlSessionFactoryBean初始化完毕后将SqlSessionFactory传递进去初始化SqlSessionTemplate
  4. 当我们执行service层中业务方法的时候,就会进入到我们的AOP切面的前置通知,将注解上标注的数据源名称赋值给contextHolder
  5. 然后StatementHandler对象执行query()方法,在执行query()方法的过程中会去获取数据库连接,然后就来到了我们重写的 determineCurrentLookupKey()方法,我们之前早就在contextHolder中给设置好,所以我们直接从里面取就好了。
  6. AbstractRoutingDataSourcedetermineTargetDataSource()方法中得到我们返回的数据源名称,然后去resolvedDataSources中去,resolvedDataSources是一个map集合,key就是我们的数据源名称,value就是真实的数据源
  7. 最终我们的StatementHandler获取到数据源连接,就可以去执行具体的SQL了。

以上准备工作做好,我们就可以进行测试业务代码了。

实体类

public class User implements Serializable 

    private int id;
    private String name;
    private int age;
    private String email;

    public int getId() 
        return id;
    

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

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public int getAge() 
        return age;
    

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

    public String getEmail() 
        return email;
    

    public void setEmail(String email) 
        this.email = email;
    
    public User() 

    
    public User(int id, String name, int age, String email) 
        this.id = id;
        this.name = name;
        this.age = age

以上是关于springBoot-mybatis+druid多数据源的主要内容,如果未能解决你的问题,请参考以下文章

springboot-mybatis配置问题

SpringBoot-mybatis

springboot-mybatis配置(xml)/springboot-jpa配置

springboot-mybatis 批量insert

SpringBoot-mybatis的代码生成器EasyCode使用示例

SpringBoot+Mybatis+ Druid+PageHelper 实现多数据源并分页