SpringBoot 之多數(shù)據(jù)源動態(tài)切換數(shù)據(jù)源

感謝原創(chuàng)
SpringBoot 之多數(shù)據(jù)源動態(tài)切換數(shù)據(jù)源

【SpringBoot2.0 系列 01】初識 SpringBoot
【SpringBoot2.0 系列 02】SpringBoot 之使用 Thymeleaf 視圖模板
【SpringBoot2.0 系列 03】SpringBoot 之使用 freemark 視圖模板
【SpringBoot2.0 系列 04】SpringBoot 之使用 JPA 完成簡單的 rest api
【SpringBoot2.0 系列 05】SpringBoot 之整合 Mybatis
【SpringBoot2.0 系列 06】SpringBoot 之多數(shù)據(jù)源動態(tài)切換數(shù)據(jù)源

前言

在前面兩節(jié)我們已經(jīng)完成 springboot 操作 mysql 數(shù)據(jù)庫,但是在實(shí)際業(yè)務(wù)場景中,數(shù)據(jù)量迅速增長,一個庫一個表已經(jīng)滿足不了我們的需求的時候,我們就會考慮分庫分表的操作,那么接下來我們就去學(xué)習(xí)一下,在 springboot 中如何實(shí)現(xiàn)多數(shù)據(jù)源,動態(tài)數(shù)據(jù)源切換,讀寫分離等操作。

實(shí)現(xiàn)

1、建庫建表

首先,我們在本地新建三個數(shù)據(jù)庫名分別為master,slave1,slave2,我們的目前就是寫入操作都是在master,查詢是 slave1,slave2
因此我們在上一篇也就是【SpringBoot2.0 系列 05】SpringBoot 之整合 Mybatis 基礎(chǔ)上進(jìn)行改動,
我們在master slave1 slave2中都創(chuàng)建user表 其中初始化slave1庫的user表數(shù)據(jù)為

[圖片上傳失敗...(image-22ffa9-1632203505883)]

初始化
slave2庫的user

[圖片上傳失敗...(image-1f3306-1632203505883)]

具體的數(shù)據(jù)庫腳本如下

create table master.user
(
    id bigint auto_increment comment '主鍵'
        primary key,
    age int null comment '年齡',
    password varchar(32) null comment '密碼',
    sex int null comment '性別',
    username varchar(32) null comment '用戶名'
)
engine=MyISAM collate=utf8mb4_bin
;

create table slave1.user
(
    id bigint auto_increment comment '主鍵'
        primary key,
    age int null comment '年齡',
    password varchar(32) null comment '密碼',
    sex int null comment '性別',
    username varchar(32) null comment '用戶名'
)
engine=MyISAM collate=utf8mb4_bin
;

INSERT INTO slave1.user (id, age, password, sex, username) VALUES (2, 22, 'admin', 1, 'admin');

create table slave2.user
(
    id bigint auto_increment comment '主鍵'
        primary key,
    age int null comment '年齡',
    password varchar(32) null comment '密碼',
    sex int null comment '性別',
    username varchar(32) null comment '用戶名'
)
engine=MyISAM collate=utf8mb4_bin
;
INSERT INTO slave2.user (id, age, password, sex, username) VALUES (3, 19, 'uuu', 2, 'user');
INSERT INTO slave2.user (id, age, password, sex, username) VALUES (4, 18, 'bbbb', 1, 'zzzz');

2、配置多數(shù)據(jù)源

經(jīng)過上面初始化 我們的master.user是一張空表,我們等下的插入與更新操作就在這上面,那么我們的查詢操作就是在slave1.user跟slave2.user上面了。
上面我們的數(shù)據(jù)庫初始化工作完成了,接下來就是實(shí)現(xiàn)動態(tài)數(shù)據(jù)源的過程
首先我們需要在我們的application.yml配置我們的三個數(shù)據(jù)源

server:
  port: 8989
spring:
  datasource:
    master:
      password: root
      url: jdbc:mysql://127.0.0.1:3306/master?useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      type: com.zaxxer.hikari.HikariDataSource
    cluster:
    - key: slave1
      password: root
      url: jdbc:mysql://127.0.0.1:3306/slave1?useUnicode=true&characterEncoding=UTF-8
      idle-timeout: 20000
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      type: com.zaxxer.hikari.HikariDataSource
    - key: slave2
      password: root
      url: jdbc:mysql://127.0.0.1:3306/slave2?useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
mybatis:
  mapper-locations: classpath:/mybatis/mapper/*.xml
  config-location:  classpath:/mybatis/config/mybatis-config.xml

在上面我們配置了三個數(shù)據(jù),其中第一個作為默認(rèn)數(shù)據(jù)源也就是我們的master數(shù)據(jù)源。主要是寫操作,那么讀操作交給我們的slave1跟slave2
其中 master 數(shù)據(jù)源一定是要配置 作為我們的默認(rèn)數(shù)據(jù)源,其次 cluster 集群中,其他的數(shù)據(jù)不配置也不會影響程序員運(yùn)行,如果你想添加新的一個數(shù)據(jù)源 就在 cluster 下新增一個數(shù)據(jù)源即可,其中 key 為必須項(xiàng),用于數(shù)據(jù)源的唯一標(biāo)識,以及接下來切換數(shù)據(jù)源的標(biāo)識。

3、注冊數(shù)據(jù)源

在上面我們已經(jīng)配置了三個數(shù)據(jù)源,但是這是我們自定義的配置,springboot 是無法給我們自動配置,所以需要我們自己注冊數(shù)據(jù)源.
那么就要實(shí)現(xiàn) EnvironmentAware用于讀取上下文環(huán)境變量用于構(gòu)建數(shù)據(jù)源,同時也需要實(shí)現(xiàn) ImportBeanDefinitionRegistrar接口注冊我們構(gòu)建的數(shù)據(jù)源。com.yukong.chapter5.register.DynamicDataSourceRegister具體代碼如下

/**
 * 動態(tài)數(shù)據(jù)源注冊
 * 實(shí)現(xiàn) ImportBeanDefinitionRegistrar 實(shí)現(xiàn)數(shù)據(jù)源注冊
 * 實(shí)現(xiàn) EnvironmentAware 用于讀取application.yml配置
 */
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);

    /**
     * 配置上下文(也可以理解為配置文件的獲取工具)
     */
    private Environment evn;

    /**
     * 別名
     */
    private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();

    /**
     * 由于部分?jǐn)?shù)據(jù)源配置不同,所以在此處添加別名,避免切換數(shù)據(jù)源出現(xiàn)某些參數(shù)無法注入的情況
     */
    static {
        aliases.addAliases("url", new String[]{"jdbc-url"});
        aliases.addAliases("username", new String[]{"user"});
    }

    /**
     * 存儲我們注冊的數(shù)據(jù)源
     */
    private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();

    /**
     * 參數(shù)綁定工具 springboot2.0新推出
     */
    private Binder binder;

    /**
     * ImportBeanDefinitionRegistrar接口的實(shí)現(xiàn)方法,通過該方法可以按照自己的方式注冊bean
     *
     * @param annotationMetadata
     * @param beanDefinitionRegistry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        // 獲取所有數(shù)據(jù)源配置
        Map config, defauleDataSourceProperties;
        defauleDataSourceProperties = binder.bind("spring.datasource.master", Map.class).get();
        // 獲取數(shù)據(jù)源類型
        String typeStr = evn.getProperty("spring.datasource.master.type");
        // 獲取數(shù)據(jù)源類型
        Class<? extends DataSource> clazz = getDataSourceType(typeStr);
        // 綁定默認(rèn)數(shù)據(jù)源參數(shù) 也就是主數(shù)據(jù)源
        DataSource consumerDatasource, defaultDatasource = bind(clazz, defauleDataSourceProperties);
        DynamicDataSourceContextHolder.dataSourceIds.add("master");
        logger.info("注冊默認(rèn)數(shù)據(jù)源成功");
        // 獲取其他數(shù)據(jù)源配置
        List<Map> configs = binder.bind("spring.datasource.cluster", Bindable.listOf(Map.class)).get();
        // 遍歷從數(shù)據(jù)源
        for (int i = 0; i < configs.size(); i++) {
            config = configs.get(i);
            clazz = getDataSourceType((String) config.get("type"));
            defauleDataSourceProperties = config;
            // 綁定參數(shù)
            consumerDatasource = bind(clazz, defauleDataSourceProperties);
            // 獲取數(shù)據(jù)源的key,以便通過該key可以定位到數(shù)據(jù)源
            String key = config.get("key").toString();
            customDataSources.put(key, consumerDatasource);
            // 數(shù)據(jù)源上下文,用于管理數(shù)據(jù)源與記錄已經(jīng)注冊的數(shù)據(jù)源key
            DynamicDataSourceContextHolder.dataSourceIds.add(key);
            logger.info("注冊數(shù)據(jù)源{}成功", key);
        }
        // bean定義類
        GenericBeanDefinition define = new GenericBeanDefinition();
        // 設(shè)置bean的類型,此處DynamicRoutingDataSource是繼承AbstractRoutingDataSource的實(shí)現(xiàn)類
        define.setBeanClass(DynamicRoutingDataSource.class);
        // 需要注入的參數(shù)
        MutablePropertyValues mpv = define.getPropertyValues();
        // 添加默認(rèn)數(shù)據(jù)源,避免key不存在的情況沒有數(shù)據(jù)源可用
        mpv.add("defaultTargetDataSource", defaultDatasource);
        // 添加其他數(shù)據(jù)源
        mpv.add("targetDataSources", customDataSources);
        // 將該bean注冊為datasource,不使用springboot自動生成的datasource
        beanDefinitionRegistry.registerBeanDefinition("datasource", define);
        logger.info("注冊數(shù)據(jù)源成功,一共注冊{}個數(shù)據(jù)源", customDataSources.keySet().size() + 1);
    }

    /**
     * 通過字符串獲取數(shù)據(jù)源class對象
     *
     * @param typeStr
     * @return
     */
    private Class<? extends DataSource> getDataSourceType(String typeStr) {
        Class<? extends DataSource> type;
        try {
            if (StringUtils.hasLength(typeStr)) {
                // 字符串不為空則通過反射獲取class對象
                type = (Class<? extends DataSource>) Class.forName(typeStr);
            } else {
                // 默認(rèn)為hikariCP數(shù)據(jù)源,與springboot默認(rèn)數(shù)據(jù)源保持一致
                type = HikariDataSource.class;
            }
            return type;
        } catch (Exception e) {
            throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //無法通過反射獲取class對象的情況則拋出異常,該情況一般是寫錯了,所以此次拋出一個runtimeexception
        }
    }

    /**
     * 綁定參數(shù),以下三個方法都是參考DataSourceBuilder的bind方法實(shí)現(xiàn)的,目的是盡量保證我們自己添加的數(shù)據(jù)源構(gòu)造過程與springboot保持一致
     *
     * @param result
     * @param properties
     */
    private void bind(DataSource result, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
        // 將參數(shù)綁定到對象
        binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
    }

    private <T extends DataSource> T bind(Class<T> clazz, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
        // 通過類型綁定參數(shù)并獲得實(shí)例對象
        return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
    }

    /**
     * @param clazz
     * @param sourcePath 參數(shù)路徑,對應(yīng)配置文件中的值,如: spring.datasource
     * @param <T>
     * @return
     */
    private <T extends DataSource> T bind(Class<T> clazz, String sourcePath) {
        Map properties = binder.bind(sourcePath, Map.class).get();
        return bind(clazz, properties);
    }

    /**
     * EnvironmentAware接口的實(shí)現(xiàn)方法,通過aware的方式注入,此處是environment對象
     *
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
        logger.info("開始注冊數(shù)據(jù)源");
        this.evn = environment;
        // 綁定配置器
        binder = Binder.get(evn);
    }
}

上面代碼需要注意的是在springboot2.x系列中用于綁定的工具類如 RelaxedPropertyResolver 已經(jīng)無法現(xiàn)在使用Binder代替。上面代碼主要是讀取 application 中數(shù)據(jù)源的配置,先讀取spring.datasource.master 構(gòu)建默認(rèn)數(shù)據(jù)源, 然后在構(gòu)建cluster中的數(shù)據(jù)源。
在這里注冊完數(shù)據(jù)源之后,我們需要通過 @import 注解把我們的數(shù)據(jù)源注冊器導(dǎo)入到 spring 中 在啟動類Chapter5Application.java加上如下注解@Import(DynamicDataSourceRegister.class)。
其中我們用到了一個DynamicDataSourceContextHolder 中的靜態(tài)變量來保存我們已經(jīng)注冊成功的數(shù)據(jù)源的key, 至此我們的數(shù)據(jù)源注冊就已經(jīng)完成了。

4、配置數(shù)據(jù)源上下文

我們需要新建一個數(shù)據(jù)源上下文,用戶記錄當(dāng)前線程使用的數(shù)據(jù)源的 key 是什么,以及記錄所有注冊成功的數(shù)據(jù)源的 key 的集合。對于線程級別的私有變量,我們首先ThreadLocal來實(shí)現(xiàn)。
com.yukong.chapter5.config.DynamicDataSourceContextHolder代碼取下

/**
 * @Auther: yukong
 * @Date: 2018/8/15 10:49
 * @Description: 數(shù)據(jù)源上下文
 */
public class DynamicDataSourceContextHolder {

    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

    /**
     * 存儲已經(jīng)注冊的數(shù)據(jù)源的key
     */
    public static List<String> dataSourceIds = new ArrayList<>();

    /**
     * 線程級別的私有變量
     */
    private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();

    public static String getDataSourceRouterKey () {
        return HOLDER.get();
    }

    public static void setDataSourceRouterKey (String dataSourceRouterKey) {
        logger.info("切換至{}數(shù)據(jù)源", dataSourceRouterKey);
        HOLDER.set(dataSourceRouterKey);
    }

    /**
     * 設(shè)置數(shù)據(jù)源之前一定要先移除
     */
    public static void removeDataSourceRouterKey () {
        HOLDER.remove();
    }

    /**
     * 判斷指定DataSrouce當(dāng)前是否存在
     *
     * @param dataSourceId
     * @return
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceIds.contains(dataSourceId);
    }

}

5、動態(tài)數(shù)據(jù)源路由

前面我們以及新建了數(shù)據(jù)源上下文,用于存儲我們當(dāng)前線程的數(shù)據(jù)源 key 那么怎么通知spring用 key 當(dāng)前的數(shù)據(jù)源呢,查閱資料可知,spring提供一個接口,名為AbstractRoutingDataSource的抽象類,我們只需要重寫determineCurrentLookupKey方法就可以,這個方法看名字就知道,就是返回當(dāng)前線程的數(shù)據(jù)源的 key,那我們只需要從我們剛剛的數(shù)據(jù)源上下文中取出我們的 key 即可,那么具體代碼取下。
com.yukong.chapter5.config.DynamicRoutingDataSource

/**
 * @Auther: yukong
 * @Date: 2018/8/15 10:47
 * @Description: 動態(tài)數(shù)據(jù)源路由配置
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private static Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceName = DynamicDataSourceContextHolder.getDataSourceRouterKey();
        logger.info("當(dāng)前數(shù)據(jù)源是:{}", dataSourceName);
        return DynamicDataSourceContextHolder.getDataSourceRouterKey();
    }
}

6、通過 aop + 注解實(shí)現(xiàn)動態(tài)數(shù)據(jù)源的切換

現(xiàn)在 spring 也已經(jīng)知道通過 key 來取對應(yīng)的數(shù)據(jù)源,我們現(xiàn)在只需要實(shí)現(xiàn)給對應(yīng)的類或者方法設(shè)置他們的數(shù)據(jù)源的 key,并且保存在數(shù)據(jù)源上下文中即可。這里我們采用注解來設(shè)置數(shù)據(jù)源,通過 aop 攔截并且保存到數(shù)據(jù)源上下中。
我們新建一個標(biāo)識數(shù)據(jù)源的注解@DataSource具體代碼取下
com.yukong.chapter5.annotation.DataSource

/**
 * 切換數(shù)據(jù)注解 可以用于類或者方法級別 方法級別優(yōu)先級 > 類級別
 */
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master"; //該值即key值
}

其中他的默認(rèn)值是master, 因?yàn)槲覀兡J(rèn)數(shù)據(jù)源的 key 也是master。也就是說如果你直接用注解,而不指定 value 的話,那么默認(rèn)就使用 master 默認(rèn)數(shù)據(jù)源。
然后我們新建一個 aop 類來攔截。代碼如下
com.yukong.chapter5.aop

package com.yukong.chapter5.aop;

import com.yukong.chapter5.annotation.DataSource;
import com.yukong.chapter5.config.DynamicDataSourceContextHolder;
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;
import org.springframework.util.StringUtils;

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

    @Before("@annotation(ds)")
    public void changeDataSource(JoinPoint point, DataSource ds) throws Throwable {
        String dsId = ds.value();
        if (DynamicDataSourceContextHolder.dataSourceIds.contains(dsId)) {
            logger.debug("Use DataSource :{} >", dsId, point.getSignature());
        } else {
            logger.info("數(shù)據(jù)源[{}]不存在,使用默認(rèn)數(shù)據(jù)源 >{}", dsId, point.getSignature());
            DynamicDataSourceContextHolder.setDataSourceRouterKey(dsId);
        }
    }

    @After("@annotation(ds)")
    public void restoreDataSource(JoinPoint point, DataSource ds) {
        logger.debug("Revert DataSource : " + ds.value() + " > " + point.getSignature());
        DynamicDataSourceContextHolder.removeDataSourceRouterKey();

    }
}

通過 aop 攔截,獲取注解上面的 value 的值 key,然后取判斷我們注冊的 keys 集合中是否有這個 key, 如果沒有,則使用默認(rèn)數(shù)據(jù)源,如果有,則設(shè)置上下文中當(dāng)前數(shù)據(jù)源的 key 為注解的 value。
7、測試
最后我們在對應(yīng)的方法上面加上注解來測試一下即可
我們在 UserMapper.java 上面加上注解,并且進(jìn)行測試。

/**
 * @Auther: yukong
 * @Date: 2018/8/13 19:47
 * @Description: UserMapper接口
 */
public interface UserMapper {

    /**
     * 新增用戶
     * @param user
     * @return
     */
    @DataSource  //默認(rèn)數(shù)據(jù)源
    int save(User user);

    /**
     * 更新用戶信息
     * @param user
     * @return
     */
    @DataSource  //默認(rèn)數(shù)據(jù)源
    int update(User user);

    /**
     * 根據(jù)id刪除
     * @param id
     * @return
     */
    @DataSource  //默認(rèn)數(shù)據(jù)源
    int deleteById(Long id);

    /**
     * 根據(jù)id查詢
     * @param id
     * @return
     */
    @DataSource("slave1")  //slave1
    User selectById(Long id);

    /**
     * 查詢所有用戶信息
     * @return
     */
    @DataSource("slave2")  //slave2
    List<User> selectAll();
}

上面代碼可以知道,我們的新增,修改,刪除方法都是在默認(rèn)數(shù)據(jù) master 上,我們的 id 查詢是在 slave1,我們的查詢所有在 slave2,我們編寫測試類來測試把。

/**
 * @Auther: yukong
 * @Date: 2018/8/14 16:34
 * @Description:
 */
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void save() {
        User user = new User();
        user.setUsername("master");
        user.setPassword("master");
        user.setSex(1);
        user.setAge(18);
        Assert.assertEquals(1,userMapper.save(user));
    }

    @Test
    public void update() {
        User user = new User();
        user.setId(8L);
        user.setPassword("newpassword");
        // 返回插入的記錄數(shù) ,期望是1條 如果實(shí)際不是一條則拋出異常
        Assert.assertEquals(1,userMapper.update(user));
    }

    @Test
    public void selectById() {
        User user = userMapper.selectById(2L);
        System.out.println("id:" + user.getId());
        System.out.println("name:" + user.getUsername());
        System.out.println("password:" + user.getPassword());
    }

    @Test
    public void deleteById() {
        Assert.assertEquals(1,userMapper.deleteById(1L));
    }

    @Test
    public void selectAll() {
        List<User> users= userMapper.selectAll();
        users.forEach(user -> {
            System.out.println("id:" + user.getId());
            System.out.println("name:" + user.getUsername());
            System.out.println("password:" + user.getPassword());
        });
    }

}

首先測試 save 方法,它將會把數(shù)據(jù)存到 master 庫的 user 表,
現(xiàn)在 user 表是空的,如圖


image.png

運(yùn)行 save 方法。

image.png

綠色,測試通過,并且日志提示數(shù)據(jù)源注冊成功,一共三個。并且當(dāng)前使用的 master 數(shù)據(jù)源,我們再去 master 數(shù)據(jù)庫看看有沒有數(shù)據(jù)。

image.png

如上圖,插入成功。
新增方法測試完成了。我們在測試一下修改與刪除。

[圖片上傳失敗...(image-3947c9-1632203884939)]

修改方法也測試通過,查看數(shù)據(jù)庫。

[圖片上傳失敗...(image-2b41bf-1632203884939)]

修改成功,刪除方法我就不測試, 我們在測試測試,slave1 跟 slave2 數(shù)據(jù)源的方法,
首先測試slave1的主鍵查詢方法,先看數(shù)據(jù)庫 slave1有哪些數(shù)據(jù)。

[圖片上傳失敗...(image-8791a2-1632203884939)]

slave1.user就一條 id 為 2 的數(shù)據(jù)并且 id 為 2 的數(shù)據(jù)就 slave1 才有,我們測試一下能不能查到。

[圖片上傳失敗...(image-3b3b88-1632203884939)]

運(yùn)行通過,數(shù)據(jù)源為slave1并且數(shù)據(jù)也正確顯示。
最后我們來測試一下slave2的 selectAll 方法把,同樣先看看slave2.user中有什么數(shù)據(jù)。

[圖片上傳失敗...(image-f915c4-1632203884939)]

從圖中,得知slave2.user中有兩條數(shù)據(jù),id 分別為 3,4。接下來運(yùn)行測試方法。
結(jié)果如圖。

[圖片上傳失敗...(image-aa225e-1632203884939)]

日志提示數(shù)據(jù)源切換值slave2,并且 id 為 3,4 的數(shù)據(jù)也成功打印。
那么至此我們的多數(shù)據(jù)源動態(tài)數(shù)據(jù)源就完成了。

主要的思路就是

  1. 配置文件中配置多個數(shù)據(jù)源
  2. 啟動類注冊動態(tài)數(shù)據(jù)源
  3. 在需要的方法上使用注解指定數(shù)據(jù)源
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容