SpringBoot多數據源自動切換


現在的企業服務逐漸地呈現出數據的指數級增長趨勢,無論從數據庫的選型還是搭建,大多數的團隊都開始考慮多樣化的數據庫來支撐存儲服務。例如分布式數據庫、Nosql數據庫、內存數據庫、關系型數據庫等等。再到后端開發來說,服務的增多,必定需要考慮到多數據源的切換使用來兼容服務之間的調用。

一、引入依賴

<!-- 核心啟動器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- jdbc 操作數據庫API -->
<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>
<!-- aop 切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

二、application.properties

spring:
  datasource:
    # default數據源, 這里有點郁悶,將如果默認數據源是這樣的形式spring.datasource.default.url,會導致錯誤
    # 根據網上的一些說法啟動時不會開啟自動配置數據庫:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    # 但是還是不行,最后只能這樣配置了
    url: jdbc:mysql://192.168.178.5:12345/mydb?characterEncoding=UTF-8&useUnicode=true&useSSL=false
    username: root
    password: 123456
    driver: com.mysql.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource

    #其余數據源
    names: tb1,tb2
    tb1:
      url: jdbc:mysql://192.168.178.5:12345/mydb2?characterEncoding=UTF-8&useUnicode=true&useSSL=false
      username: root
      password: 123456
      driver:  com.mysql.jdbc.Driver

    tb2:
      url: jdbc:mysql://192.168.178.5:12345/mydb3?characterEncoding=UTF-8&useUnicode=true&useSSL=false
      username: root
      password: 123456
      driver:  com.mysql.jdbc.Driver

三、具體代碼實施

1. 使用ThreadLocal創建一個線程安全的類,存放當前線程的數據源類型

public class DynamicDataSourceContextHolder {

    //存放當前線程使用的數據源類型信息
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    //存放數據源id
    public static List<String> dataSourceIds = new ArrayList<String>();

    //設置數據源
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    //獲取數據源
    public static String getDataSourceType() {
        return contextHolder.get();
    }

    //清除數據源
    public static void clearDataSourceType() {
        contextHolder.remove();
        System.out.println("清除數據源:" + contextHolder.get());
    }

    //判斷當前數據源是否存在
    public static boolean isContainsDataSource(String dataSourceId) {
        return dataSourceIds.contains(dataSourceId);
    }
}

2. 創建一個DynamicDataSource重寫AbstractRoutingDataSource的determineCurrentLookupKey()方法

/**
 * AbstractRoutingDataSource的內部維護了一個名為targetDataSources的Map,
 * 並提供的setter方法用於設置數據源關鍵字與數據源的關系,實現類被要求實現其determineCurrentLookupKey()方法,
 * 由此方法的返回值決定具體從哪個數據源中獲取連接。
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = DynamicDataSourceContextHolder.getDataSourceType();
        System.out.println("當前數據源:" + dataSource);
        return dataSource;
    }

}

3. 創建一個Register實現ImportBeanDefinitionRegistrar, EnvironmentAware

/**
 * ImportBeanDefinitionRegistrar介紹
 *  1.ImportBeanDefinitionRegistrar接口不是直接注冊Bean到IOC容器,它的執行時機比較早,
 *      准確的說更像是注冊Bean的定義信息以便后面的Bean的創建。
 *  2. ImportBeanDefinitionRegistrar接口提供了registerBeanDefinitions方便讓子類進
 *      行重寫。該方法提供BeanDefinitionRegistry類型的參數,讓開發者調用BeanDefinitionRegistry的registerBeanDefinition方法傳入BeanDefinitionName和對應的BeanDefinition對象,直接往容器中注冊。
 *  3. ImportBeanDefinitionRegistrar只能通過由其它類import的方式來加載,通常是主啟動類類或者注解。
 *  4. 這里要特別注意:ImportBeanDefinitionRegistrar有兩個重載的registerBeanDefinitions方法,我們只需要重寫其中一個即可否則容易出錯
 *      (1) 如果重寫了兩個方法容易出現這個問題,三個參數的registerBeanDefinitions方法為空邏輯,兩個參數的registerBeanDefinitions方法有實際的
 *          代碼邏輯,這樣會導致代碼邏輯不能實際被執行,需要在三個參數的那個方法再調一個兩個參數的方法
 *      (2) 如果重寫了兩個方法,就只能必須將實際的代碼邏輯寫在有三個參數的registerBeanDefinitions方法中,然后兩個參數的方法不寫任何邏輯即可
 *      通用查看ImportBeanDefinitionRegistrar的實際源碼即可知道問題所在。
 */

public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    //指定默認數據源(springboot2.0默認數據源是hikari,大家也可以使用DruidDataSource)
    private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
    //默認數據源 javax.sql.DataSource
    private DataSource defaultDataSource;
    //用戶自定義數據源
    private Map<String, DataSource> slaveDataSources = new HashMap<>();

    @Override
    public void setEnvironment(Environment environment) {
        this.initDefaultDataSource(environment);
        this.initMutilDataSources(environment);
    }

    private void initDefaultDataSource(Environment env) {
        // 讀取主數據源,解析yml文件
        Map<String, Object> dsMap = new HashMap<>();
        dsMap.put("driver", env.getProperty("spring.datasource.driver"));
        dsMap.put("url", env.getProperty("spring.datasource.url"));
        dsMap.put("username", env.getProperty("spring.datasource.username"));
        dsMap.put("password", env.getProperty("spring.datasource.password"));
        dsMap.put("type", env.getProperty("spring.datasource.type"));
        defaultDataSource = buildDataSource(dsMap);
    }

    private void initMutilDataSources(Environment env) {
        // 讀取配置文件獲取更多數據源
        String dsPrefixs = env.getProperty("spring.datasource.names");
        for (String dsPrefix : dsPrefixs.split(",")) {
            // 多個數據源
            Map<String, Object> dsMap = new HashMap<>();
            dsMap.put("driver", env.getProperty("spring.datasource." + dsPrefix.trim() + ".driver"));
            dsMap.put("url", env.getProperty("spring.datasource." + dsPrefix.trim() + ".url"));
            dsMap.put("username", env.getProperty("spring.datasource." + dsPrefix.trim() + ".username"));
            dsMap.put("password", env.getProperty("spring.datasource." + dsPrefix.trim() + ".password"));
            DataSource ds = buildDataSource(dsMap);
            slaveDataSources.put(dsPrefix, ds);
        }
    }

    private DataSource buildDataSource(Map<String, Object> dataSourceMap) {
        try {
            Object type = dataSourceMap.get("type");
            if (type == null) {
                type = DATASOURCE_TYPE_DEFAULT;// 默認DataSource
            }

            Class<? extends DataSource> dataSourceType;
            dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
            String driverClassName = dataSourceMap.get("driver").toString();
            String url = dataSourceMap.get("url").toString();
            String username = dataSourceMap.get("username").toString();
            String password = dataSourceMap.get("password").toString();
            // 自定義DataSource配置
            DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
                    .username(username).password(password).type(dataSourceType);
            return factory.build();
        } catch (ClassNotFoundException e) {
            System.out.println("找不到指定的類");
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 注入DynamicDataSource的 bean 定義,
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        //添加默認數據源
        targetDataSources.put("default", this.defaultDataSource);
        DynamicDataSourceContextHolder.dataSourceIds.add("default");
        //添加其他數據源
        targetDataSources.putAll(slaveDataSources);
        DynamicDataSourceContextHolder.dataSourceIds.addAll(slaveDataSources.keySet());
        //創建DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        //defaultTargetDataSource 和 targetDataSources屬性是 AbstractRoutingDataSource的兩個屬性Map
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        //注冊 - BeanDefinitionRegistry
        registry.registerBeanDefinition("dataSource", beanDefinition);
        System.out.println("Dynamic DataSource Registry");
    }
}

4. 創建注解類@DbName

/**
 * @DbName注解用於類、方法上
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DbName {
    String value();
}

5. 創建aop切面類ChooseDbAspect

@Component
@Order(-1)//這里一定要保證在@Transactional之前執行
@Aspect
public class ChooseDbAspect {
    /**
     * 切入點
     */
    @Pointcut("@annotation(com.example.multidb.anno.DbName)")
    public void chooseDbPointCut(){ }

    @Before("@annotation(dbName)")
    public void changeDataSource(JoinPoint joinPoint, DbName dbName) {
        String dbid = dbName.value();
        if (!DynamicDataSourceContextHolder.isContainsDataSource(dbid)) {
            //joinPoint.getSignature() :獲取連接點的方法簽名對象
            System.out.println("數據源 " + dbid + " 不存在使用默認的數據源 -> " + joinPoint.getSignature());
        } else {
            System.out.println("使用數據源:" + dbid);
            //向當前線程設置使用的數據源信息。
            //當業務操作時,會由AbstractRoutingDataSource的determineCurrentLookupKey方法,返回從哪個數據源獲取連接。
            //又因DynamicDataSource重寫了determineCurrentLookupKey方法,返回的是ThreadLocal<String>的值
            //所以這樣設置能夠決定哪個數據源起作用
            DynamicDataSourceContextHolder.setDataSourceType(dbid);
        }
    }

    @After("@annotation(dbName)")
    public void clearDataSource(JoinPoint joinPoint, DbName dbName) {
        System.out.println("清除數據源 " + dbName.value() + " ! - start");
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

6. 因DynamicDataSourceRegister的注入需要使用@Import,為了方便,我們創建一個注解

@Target({java.lang.annotation.ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({DynamicDataSourceRegister.class})
public @interface EnableDynamicDataSource {

}

然后在啟動類上添加注解@EnableDynamicDataSource 。

四、單元測試

1. 創建DAO

接口

public interface UserDao {
    List<Map<String,Object>> listUsers();
}

實現類:

@Repository
public class UserDaoImpl implements UserDao{

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public List<Map<String, Object>> listUsers() {
        String sql = "select id, name, age from user";
        return jdbcTemplate.queryForList(sql);

    }
}

2. 創建Service

接口:

public interface UserService {
    List<Map<String,Object>> listUsers();
}

實現類:

@Service
public class UserServiceImpl implements UserService{

    @Autowired
    private UserDao userDao;

    @DbName(value = "tb2") //指定數據源,如果不設置,則默認是default
    @Override
    public List<Map<String, Object>> listUsers() {
        return userDao.listUsers();
    }
}

3. 測試類

@RunWith(SpringRunner.class)
@SpringBootTest
class MultidbApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    void contextLoads() {
        List<Map<String, Object>> userList = userService.listUsers();
        if(null != userList && userList.size()>0){
            for(Map<String,Object> um : userList){
                System.out.println("id:" + um.get("id"));
                System.out.println("name:" + um.get("name"));
                System.out.println("age:" + um.get("age"));
            }
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM