SpringBoot项目多数据库源动态切换

实现

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- 单元测试 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- 切面编程 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>

2、配置文件 + 数据源储存

配置文件需要配置多个数据源,用名称指定,这里使用primarysecondary

  datasource:
    primary:
      jdbc-url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true
      username: root
      password: admin
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      jdbc-url: jdbc:mysql://localhost:3306/test2?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true
      username: root
      password: admin
      driver-class-name: com.mysql.cj.jdbc.Driver

写一个 ThreadLocal 储存正在使用的数据源,也就意味着在方法使用前后需要重置清空,做到线程隔离的作用

package com.example.demo.test.config;

public class DataSourceType {
    
    
    // 内部枚举类,用于选择特定的数据类型
    public enum DataBaseType{
    
    
        Primary, Secondary
    }

    // 使用 ThreadLocal 保证线程安全(隔离)
    private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<>();

    // 往当前线程里设置数据源类型
    public static void setDataBaseType(DataBaseType dataBaseType){
    
    
        if(dataBaseType == null){
    
    
            throw new NullPointerException();
        }
        TYPE.set(dataBaseType);
    }

    // 获取数据类型
    public static DataBaseType getDataBaseType(){
    
    
        DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.Primary : TYPE.get();
        return dataBaseType;
    }

    // 清空数据类型
    public static void clearDataBaseType(){
    
    
        TYPE.remove();
    }
}

3、实现 DynamicDataSource

继承 AbstractRoutingDataSource ,重写方法,提供数据源

package com.example.demo.test.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    
    
    @Override
    protected Object determineCurrentLookupKey() {
    
    
        DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType();
        return dataBaseType;
    }
}

4、实现 AOP 和 动态数据源配置类

首先创建动态数据源配置类

package com.example.demo.test.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@MapperScan(basePackages = "com.example.demo.**.mapper", sqlSessionFactoryRef = "sqlSessionFactory")  // basePackage 接口地址
public class DynamicDataSourceConfig {
    
    

    // 将这个对象放入Spring容器
    @Bean(name = "PrimaryDataSource")
    // 表示这个是默认数据源
    @Primary
    // 读取配置文件的参数,映射为一个对象
    // prefix 表示参数的前缀
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource getDataSource(){
    
    
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "SecondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource getDataSource1(){
    
    
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    public DataSource DataSource(@Qualifier("PrimaryDataSource") DataSource primaryDataSource,
                                        @Qualifier("SecondaryDataSource") DataSource secondaryDataSource){
    
    
        // targetDataSource 集合是我们数据库和名字之间的映射
        Map<Object, Object> targetDataSource = new HashMap<>();
        // 设置两个数据库
        targetDataSource.put(DataSourceType.DataBaseType.Primary, primaryDataSource);
        targetDataSource.put(DataSourceType.DataBaseType.Secondary, secondaryDataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        // 动态切换源
        dataSource.setTargetDataSources(targetDataSource);
        // 默认是第一个数据源
        dataSource.setDefaultTargetDataSource(primaryDataSource);
        return dataSource;
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
    
    
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/**Mapper.xml"));
        return bean.getObject();
    }
}

配置好了之后,创建自定义注解,以便于随时注入,动态切换数据库。

package com.example.demo.test.config;
import java.lang.annotation.*;
// 可以在方法,参数,类或接口声明
@Target({
    
    ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
// 注释
@Documented
public @interface DataSource {
    
    
    // 默认使用的数据库
    String value() default "primary";
}

对标记了注解进行 AOP 切面,获取value,将其储存到 ThreadLocal,限定本线程的数据源

package com.example.demo.test.config;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DynamicDataSourceAspect {
    
    
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    // 拦截注释
    @Before("@annotation(dataSource)")
    public void changeDataSource(JoinPoint point, DataSource dataSource){
    
    
        // 通过注解 获取值(也就是数据源)
        String value = dataSource.value();
        if(value.equals("primary")){
    
    
            DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
        }
        else if(value.equals("secondary")){
    
    
            DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary);
        }
        // 如果没有值,默认使用主数据库
        else{
    
    
            DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
        }
    }

    @After("@annotation(dataSource)")
    public void restoreDataSoucer(JoinPoint point, DataSource dataSource){
    
    
        // 清空存在的数据源
        DataSourceType.clearDataBaseType();
    }
}

5、实体类 和 Mapper

创建一个实体类

package com.example.demo.test.pojo;
import lombok.Data;

@Data
public class Person {
    
    
    private int id;
    private String name;
}

创建相应的Mapper

package com.example.demo.test.mapper;
import com.example.demo.test.config.DataSource;
import com.example.demo.test.pojo.Person;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface PersonMapper {
    
    
    // 使用数据源二
    @DataSource("secondary")
    List<Person> query();
}

创建PersonMapper.xml(/resouce/mapper/PersonMapper.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis配置文件-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.demo.test.mapper.PersonMapper">

    <select id="query" resultType="com.example.demo.test.pojo.Person">
        select * from Person
    </select>
</mapper>

6、单元测试

package com.example.demo;
import com.example.demo.test.mapper.PersonMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class TestPersonMapper {
    
    

    @Autowired
    PersonMapper personMapper;

    @Test
    public void test(){
    
    
        personMapper.query().forEach(System.out::println);
    }
}

结果比对:

// 项目输出
Person(id=1, name=小黄)
Person(id=2, name=小花)

// 数据库
id   name
1	 小黄
2	 小花

问题

配置文件中找不到相对应的mapper

解决方案1:在pom.xml中添加

<builder>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <!-- 把 java目录下的 xml文件编译-->
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/webapp</directory>
                <targetPath>META-INF/resources</targetPath>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
</builder>

解决方案2:配置文件中可能有写错(需要有s)

  mapper-location: classpath:mapper/**/**Mapper.xml		#错误的写法
  mapper-locations: classpath:mapper/**/**Mapper.xml	#正确的写法

解决方案3:动态配置的时候写错

// 错误写法(和方案 2 一样,也是因为少了 s )
....getResouce("classpath:mapper/**/**Mapper.xml")
// 正确写法
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/**Mapper.xml"));

猜你喜欢

转载自blog.csdn.net/DespairC/article/details/124923027