SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的實(shí)戰(zhàn)案例
1.前言
大家好,今天給大家?guī)?lái)一篇關(guān)于SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的實(shí)戰(zhàn)案例。好了,話不多說(shuō)讓我們開(kāi)始吧.
2.概述
在實(shí)際開(kāi)發(fā)中,我們往往面臨一個(gè)應(yīng)用需要訪問(wèn)多個(gè)數(shù)據(jù)庫(kù)的情況。例如下面兩種場(chǎng)景。
- 業(yè)務(wù)復(fù)雜: 數(shù)據(jù)分布在不同的數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)拆了,應(yīng)用沒(méi)拆,一個(gè)公司有多個(gè)子項(xiàng)目,各用各的數(shù)據(jù)庫(kù)。
- 讀寫分離: 為了解決數(shù)據(jù)庫(kù)的讀性能瓶頸(讀比寫性能更高,寫鎖會(huì)影響讀阻塞,從而影響讀的性能)
- 很多數(shù)據(jù)庫(kù)擁有主從架構(gòu),也就是說(shuō),一臺(tái) 主數(shù)據(jù)庫(kù)服務(wù)器,是對(duì)外提供增刪改查業(yè)務(wù)的生產(chǎn)服務(wù)器;
- 另一臺(tái)從數(shù)據(jù)庫(kù)服務(wù)器,主要進(jìn)行讀的操作。
- 讀寫分離:解決高并發(fā)下讀寫受影響。數(shù)據(jù)更新在主庫(kù)上進(jìn)行,主庫(kù)將數(shù)據(jù)變更信息同步給從庫(kù)。在查詢時(shí),在從庫(kù)上進(jìn)行,從而分擔(dān)主庫(kù)的壓力。
我們可以在代碼層面解決這種動(dòng)態(tài)數(shù)據(jù)源切換的問(wèn)題,而不需要使用 mycat、shardingJDBC 等其他中間件。本文將主要以自定義注解 + 繼承 AbstractRoutingDataSource
實(shí)現(xiàn)讀寫分離。
3.如何實(shí)現(xiàn)多數(shù)據(jù)源
在 SpringBoot 項(xiàng)目中實(shí)現(xiàn)讀寫分離通常需要以下幾步:
- 配置數(shù)據(jù)源:你需要為讀操作和寫操作分別配置一個(gè)數(shù)據(jù)源。
- 創(chuàng)建數(shù)據(jù)源路由邏輯:這通常通過(guò)擴(kuò)展 Spring 的
AbstractRoutingDataSource
來(lái)實(shí)現(xiàn)。它允許你根據(jù)一定的邏輯來(lái)決定使用哪個(gè)數(shù)據(jù)源(讀或?qū)懀?/li> - 配置事務(wù)管理器:這使得你能夠在使用不同數(shù)據(jù)源時(shí)保持事務(wù)的一致性。
- 服務(wù)層或DAO層設(shè)計(jì):確保在執(zhí)行讀操作時(shí)使用讀數(shù)據(jù)源,在執(zhí)行寫操作時(shí)使用寫數(shù)據(jù)源。
- 自定義切面,在切面中解析 @DataSource 注解。當(dāng)一個(gè)方法或者類上面,有 @DataSource 注解的時(shí)候,將 @DataSource 注解所標(biāo)記的數(shù)據(jù)源列出來(lái)存入到 ThreadLocal 中。
注意:這里使用ThreadLocal的原因是為了保證我們的線程安全。
4.案例實(shí)現(xiàn)
接下來(lái)我們就按照以上步驟進(jìn)行編碼實(shí)現(xiàn)。
4.1 創(chuàng)建新模塊
首先我們創(chuàng)建一個(gè)新的模塊命名為:springboot-dynamic-source
1.導(dǎo)入依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!--mybatis plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <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> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> </dependencies>
2.創(chuàng)建yml配置文件
server: port: 8007 spring: application: name: dynamic-source jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver ds: # 主庫(kù)數(shù)據(jù)源 master: url: jdbc:mysql://localhost:3307/test01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: root # 從庫(kù)數(shù)據(jù)源 slave: url: jdbc:mysql://localhost:3307/test02?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: root # 初始連接數(shù) initialSize: 5 # 最小連接池?cái)?shù)量 minIdle: 10 # 最大連接池?cái)?shù)量 maxActive: 20 # 配置獲取連接等待超時(shí)的時(shí)間 maxWait: 60000 # 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一個(gè)連接在池中最大生存的時(shí)間,單位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置檢測(cè)連接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 設(shè)置白名單,不填則允許所有訪問(wèn) allow: url-pattern: /druid/* # 控制臺(tái)管理用戶名和密碼 login-username: admin login-password: 123456 filter: stat: enabled: true # 慢SQL記錄 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true logging: level: org.javatop: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}"
ds 中是我們的所有數(shù)據(jù)源。master 是默認(rèn)的數(shù)據(jù)源,不可修改,其他的數(shù)據(jù)源可以修改并添加多個(gè)。
3.準(zhǔn)備數(shù)據(jù)庫(kù)
我這里需要提前準(zhǔn)備兩個(gè)數(shù)據(jù)庫(kù),一個(gè)是主數(shù)據(jù)庫(kù)master,一個(gè)是從數(shù)據(jù)庫(kù)slave。
我們會(huì)后面會(huì)通過(guò)一個(gè)自定義注解去實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)庫(kù)。
這里給出我們創(chuàng)建的一個(gè)user表的SQL語(yǔ)句。
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
4.2 加載數(shù)據(jù)源
我們可以通過(guò)@ConfigurationProperties 注解加載定義的配置文件。spring.datasource 對(duì)應(yīng)的注解都會(huì)匹配到。
package org.javatop.dynamic.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import javax.sql.DataSource; import java.util.Map; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:10 * @description : */ @ConfigurationProperties(prefix = "spring.datasource") public class DruidProperties { private String type; private String driverClassName; private Map<String, Map<String,String>> ds; private Integer initialSize; private Integer minIdle; private Integer maxActive; private Integer maxWait; /** *一會(huì)在外部構(gòu)建好一個(gè) DruidDataSource 對(duì)象,包含三個(gè)核心屬性 url、username、password * 在這個(gè)方法中設(shè)置公共屬性 * @param druidDataSource * @return */ public DataSource dataSource(DruidDataSource druidDataSource){ druidDataSource.setInitialSize(initialSize); druidDataSource.setMinIdle(minIdle); druidDataSource.setMaxActive(maxActive); druidDataSource.setMaxWait(maxWait); return druidDataSource; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public Map<String, Map<String, String>> getDs() { return ds; } public void setDs(Map<String, Map<String, String>> ds) { this.ds = ds; } public Integer getInitialSize() { return initialSize; } public void setInitialSize(Integer initialSize) { this.initialSize = initialSize; } public Integer getMinIdle() { return minIdle; } public void setMinIdle(Integer minIdle) { this.minIdle = minIdle; } public Integer getMaxActive() { return maxActive; } public void setMaxActive(Integer maxActive) { this.maxActive = maxActive; } public Integer getMaxWait() { return maxWait; } public void setMaxWait(Integer maxWait) { this.maxWait = maxWait; } }
然后我們開(kāi)始通過(guò)進(jìn)行加載DruidProperties
來(lái)加載數(shù)據(jù)源。
@EnableConfigurationProperties :這個(gè)注解的意思是使 ConfigurationProperties 注解生效。
package org.javatop.dynamic.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:12 * @description : 加載數(shù)據(jù)源 */ @Component @EnableConfigurationProperties(DruidProperties.class) public class LoadDataSource { @Autowired DruidProperties druidProperties; public Map<String, DataSource> loadAllDataSource() { Map<String, DataSource> map =new HashMap<>(); Map<String, Map<String, String>> ds = druidProperties.getDs(); try { Set<String> keySet = ds.keySet(); for (String key : keySet) { map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key)))); } } catch (Exception e) { e.printStackTrace(); } return map; } }
loadAllDataSource() 方法可以通過(guò)讀取application.yml配置文件中所有數(shù)據(jù)源對(duì)象。(我們這里有一個(gè)master主數(shù)據(jù)庫(kù),和一個(gè)slave從數(shù)據(jù)庫(kù))
druidProperties.dataSource(DruidDataSource druidDataSource) 這個(gè)方法為每個(gè)數(shù)據(jù)源配置其他額外的屬性(最大連接池等信息)。
DruidDataSourceFactory.createDataSource(ds.get(key):創(chuàng)建一個(gè)數(shù)據(jù)源,賦予三個(gè)核心的屬性。(username、url、password)
最終,所有的數(shù)據(jù)源都會(huì)存入map中。
4.3 自定義ThreadLocal工具類
我們這里定義一個(gè)簡(jiǎn)單的ThreadLocal工具類
package org.javatop.dynamic.utils; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:13 * @description : ThreadLocal工具類 */ public class DynamicDataSourceUtil { private static final ThreadLocal<String> CONTEXT_HOLDER =new ThreadLocal<>(); public static void setDataSourceType(String dsType){ CONTEXT_HOLDER.set(dsType); } public static String getDataSourceType(){ return CONTEXT_HOLDER.get(); } public static void clear(){ CONTEXT_HOLDER.remove(); } }
4.4 自定義注解
首先需要通過(guò)一個(gè)枚舉類來(lái)設(shè)定一下我們的默認(rèn)數(shù)據(jù)源,也是是master主數(shù)據(jù)庫(kù)。
package org.javatop.dynamic.constant; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:13 * @description : */ public interface DataSourceType { String default_ds_name ="master"; }
然后自定義一個(gè)注解,后面也就是通過(guò)這個(gè)注解來(lái)動(dòng)態(tài)的配置切換我們的數(shù)據(jù)源,這里就也叫Datasource吧。
package org.javatop.dynamic.annotation; import org.javatop.dynamic.constant.DataSourceType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:14 * @description : 這個(gè)注解將來(lái)可以加在某一個(gè) service 類上或者方法上,通過(guò) value 屬性來(lái)指定類或者方法應(yīng)該使用哪個(gè)數(shù)據(jù)源 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface DataSource{ /** * 如果一個(gè)方法上加了 @DataSource 注解,但是卻沒(méi)有指定數(shù)據(jù)源的名稱,那么默認(rèn)使用 Master 數(shù)據(jù)源 * @return */ String value() default DataSourceType.default_ds_name; }
4.5 AOP解析自定義注解
package org.javatop.dynamic.annotation; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.javatop.dynamic.utils.DynamicDataSourceUtil; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:15 * @description : AOP解析自定義注解 */ @Component @Aspect public class DataSourceAspect { /** * @annotation(org.javatop.dynamic.annotation.DataSource) 表示方法上有 @DataSource 注解 就將方法攔截下來(lái)。 * @within :如果類上面有 @DataSource 注解,就將類中的方法攔截下來(lái)。 */ @Pointcut("@annotation(org.javatop.dynamic.annotation.DataSource) || " + "@within(org.javatop.dynamic.annotation.DataSource)") public void pc(){ } @Around("pc()") public Object around(ProceedingJoinPoint point){ //獲取方法上面的注解 DataSource dataSource =getDataSource(point); if(dataSource!=null){ // 注解中數(shù)據(jù)源的名稱 String value = dataSource.value(); DynamicDataSourceUtil.setDataSourceType(value); } try { return point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); }finally { DynamicDataSourceUtil.clear(); } return null; } private DataSource getDataSource(ProceedingJoinPoint point) { /** * 先去查找方法上的注解,如果沒(méi)有,再去類中找。 */ MethodSignature signature = (MethodSignature)point.getSignature(); DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if(annotation!=null){ return annotation; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(),DataSource.class); } }
@Pointcut 定義
@Pointcut("@annotation(org.javatop.dynamic.annotation.DataSource) || " + "@within(org.javatop.dynamic.annotation.DataSource)") public void pc() { }
@Pointcut
是一個(gè)定義在方法上的注解,用來(lái)指定一個(gè)切點(diǎn)(即在何處進(jìn)行攔截)。"@annotation(org.javatop.dynamic.annotation.DataSource)"
表示攔截所有被@DataSource
注解標(biāo)記的方法。"@within(org.javatop.dynamic.annotation.DataSource)"
表示攔截所有在類級(jí)別被@DataSource
注解標(biāo)記的類中的方法。pc()
方法本身是空的,因?yàn)樗械倪壿嫸紝⒃谂c這個(gè)切點(diǎn)相關(guān)的通知(advice)中定義。
@Around 通知
e@Around("pc()") public Object around(ProceedingJoinPoint point) { DataSource dataSource = getDataSource(point); if (dataSource != null) { String value = dataSource.value(); DynamicDataSourceUtil.setDataSourceType(value); } try { return point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } finally { DynamicDataSourceUtil.clear(); } return null; }
@Around("pc()")
表示這是一個(gè)環(huán)繞通知,它會(huì)在pc()
方法所定義的切點(diǎn)前后執(zhí)行。ProceedingJoinPoint point
是連接點(diǎn)的信息,它包含了方法的所有相關(guān)信息,如方法名、參數(shù)等。getDataSource(point)
用來(lái)獲取方法或類上的@DataSource
注解。- 如果存在
@DataSource
注解,它會(huì)從注解中獲取數(shù)據(jù)源的名稱,并通過(guò)DynamicDataSourceUtil.setDataSourceType(value)
設(shè)置當(dāng)前線程的數(shù)據(jù)源。 point.proceed()
是調(diào)用原始方法的地方。finally
塊中的DynamicDataSourceUtil.clear()
用于在方法執(zhí)行完畢后清理數(shù)據(jù)源設(shè)置,確保不會(huì)影響其他的數(shù)據(jù)庫(kù)操作。
最后獲取@DataSource注解
4.6 自定義動(dòng)態(tài)數(shù)據(jù)源
package org.javatop.dynamic.config; import org.javatop.dynamic.constant.DataSourceType; import org.javatop.dynamic.utils.DynamicDataSourceUtil; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:18 * @description : 定義動(dòng)態(tài)數(shù)據(jù)源 */ @Component public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(LoadDataSource loadDataSource) { // 1、設(shè)置所有的數(shù)據(jù)源 Map<String, DataSource> stringDataSourceMap = loadDataSource.loadAllDataSource(); super.setTargetDataSources(new HashMap<>(stringDataSourceMap)); // 2、設(shè)置默認(rèn)的數(shù)據(jù)源 super.setDefaultTargetDataSource(stringDataSourceMap.get(DataSourceType.default_ds_name)); super.afterPropertiesSet(); } /** * 這個(gè)方法用來(lái)返回?cái)?shù)據(jù)源名稱,當(dāng)系統(tǒng)需要獲取數(shù)據(jù)源的時(shí)候,會(huì)自動(dòng)調(diào)用該方法獲取數(shù)據(jù)源的名稱 * @return */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceUtil.getDataSourceType(); } }
DynamicDataSource
類擴(kuò)展自AbstractRoutingDataSource
類,這是Spring框架提供的一個(gè)抽象類,用于實(shí)現(xiàn)數(shù)據(jù)源的動(dòng)態(tài)路由。- 構(gòu)造函數(shù)
public DynamicDataSource(LoadDataSource loadDataSource)
接收一個(gè)LoadDataSource
類型的參數(shù)。這個(gè)參數(shù)被用于加載所有的數(shù)據(jù)源配置。Map<String, DataSource> stringDataSourceMap = loadDataSource.loadAllDataSource();
這行代碼調(diào)用了loadDataSource
的loadAllDataSource
方法來(lái)加載所有數(shù)據(jù)源配置,并將其存儲(chǔ)在一個(gè)名為stringDataSourceMap
的Map中,其中鍵是數(shù)據(jù)源的名稱,值是對(duì)應(yīng)的DataSource
對(duì)象。super.setTargetDataSources(new HashMap<>(stringDataSourceMap));
這行代碼設(shè)置了目標(biāo)數(shù)據(jù)源。它將前面加載的所有數(shù)據(jù)源stringDataSourceMap
設(shè)置為目標(biāo)數(shù)據(jù)源。super.setDefaultTargetDataSource(stringDataSourceMap.get(DataSourceType.default_ds_name));
這行代碼設(shè)置了默認(rèn)的數(shù)據(jù)源。它通過(guò)DataSourceType.default_ds_name
從stringDataSourceMap
中獲取默認(rèn)的數(shù)據(jù)源,并設(shè)置為默認(rèn)數(shù)據(jù)源。super.afterPropertiesSet();
是一個(gè)初始化方法,確保所有屬性都被正確設(shè)置。
determineCurrentLookupKey()
方法是AbstractRoutingDataSource
的一個(gè)抽象方法,必須要實(shí)現(xiàn)。這個(gè)方法用于決定使用哪個(gè)數(shù)據(jù)源,通常情況下是根據(jù)某種條件動(dòng)態(tài)返回?cái)?shù)據(jù)源名稱。return DynamicDataSourceUtil.getDataSourceType();
這行代碼返回當(dāng)前線程所使用的數(shù)據(jù)源的名稱。DynamicDataSourceUtil
是一個(gè)工具類,可能提供了線程局部變量(ThreadLocal)來(lái)存儲(chǔ)每個(gè)線程所選擇的數(shù)據(jù)源名稱。
這樣,當(dāng)應(yīng)用程序需要與數(shù)據(jù)庫(kù)進(jìn)行交互時(shí),就會(huì)通過(guò) DynamicDataSource
獲取到當(dāng)前線程所指定的數(shù)據(jù)源,并進(jìn)行相應(yīng)的數(shù)據(jù)庫(kù)操作。這種方式能夠在不同業(yè)務(wù)場(chǎng)景中靈活切換數(shù)據(jù)源,非常適合多租戶、讀寫分離等復(fù)雜的數(shù)據(jù)庫(kù)應(yīng)用場(chǎng)景。
4.7 編寫業(yè)務(wù)層
我們編寫一個(gè)service層
package org.javatop.dynamic.service; import org.javatop.dynamic.annotation.DataSource; import org.javatop.dynamic.domain.User; import org.javatop.dynamic.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:26 * @description : */ @Service public class UserService{ @Autowired private UserMapper userMapper; @DataSource("slave") // @DataSource public List<User> getAll(){ List<User> all = userMapper.getAll(); return all; } }
我們?cè)趃etAll()方法上加上@DataSource(“slave”),并指定slave從數(shù)據(jù)庫(kù)。
然后再編寫一個(gè)mapper,去操作數(shù)據(jù)庫(kù)。
package org.javatop.dynamic.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.javatop.dynamic.domain.User; import java.util.List; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:26 * @description : */ @Mapper public interface UserMapper { @Select("select * from user") List<User> getAll(); }
4.8 測(cè)試
package org.javatop.dynamic; import org.javatop.dynamic.domain.User; import org.javatop.dynamic.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; /** * @author : Leo * @version 1.0 * @date 2024-01-02 15:32 * @description : */ @SpringBootTest public class DynamicTest { @Autowired private UserService userService; /** * 用于測(cè)試: */ @Test public void test() { List<User> all = userService.getAll(); if(all !=null){ for (User user : all) { System.out.println(user); } } } }
我們查看控制臺(tái)。
可以看出來(lái)我們?nèi)ゲ樵兊氖莟est02庫(kù)中的user數(shù)據(jù)。
大功告成!!!
5.總結(jié)
以上就是SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的實(shí)戰(zhàn)案例的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Bloc事件流是一個(gè)阻塞隊(duì)列結(jié)論解析
這篇文章主要為大家介紹了Bloc事件流是一個(gè)阻塞隊(duì)列結(jié)論解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11springboot中不能獲取post請(qǐng)求參數(shù)的解決方法
這篇文章主要介紹了springboot中不能獲取post請(qǐng)求參數(shù)的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06java 使用idea將工程打成jar并創(chuàng)建成exe文件類型執(zhí)行的方法詳解
這篇文章主要介紹了java 使用idea將工程打成jar并創(chuàng)建成exe文件類型執(zhí)行,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-09-09spring boot項(xiàng)目導(dǎo)入依賴后代碼報(bào)錯(cuò)問(wèn)題的解決方法
這篇文章主要給大家介紹了關(guān)于spring boot項(xiàng)目導(dǎo)入依賴后代碼報(bào)錯(cuò)問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08SpringBoot使用mybatis-plus分頁(yè)查詢無(wú)效的問(wèn)題解決
MyBatis-Plus提供了很多便捷的功能,包括分頁(yè)查詢,本文主要介紹了SpringBoot使用mybatis-plus分頁(yè)查詢無(wú)效的問(wèn)題解決,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12Java使用itext5實(shí)現(xiàn)PDF表格文檔導(dǎo)出
這篇文章主要介紹了Java使用itext5實(shí)現(xiàn)PDF表格文檔導(dǎo)出,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01Java動(dòng)態(tài)代理實(shí)現(xiàn)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
動(dòng)態(tài)代理作為代理模式的一種擴(kuò)展形式,廣泛應(yīng)用于框架(尤其是基于AOP的框架)的設(shè)計(jì)與開(kāi)發(fā),本文將通過(guò)實(shí)例來(lái)講解Java動(dòng)態(tài)代理的實(shí)現(xiàn)過(guò)程2017-08-08Java中四種訪問(wèn)控制權(quán)限解析(private、default、protected、public)
java當(dāng)中有4種訪問(wèn)修飾限定符privat、default(默認(rèn)訪問(wèn)權(quán)限),protected以及public,本文就詳細(xì)的介紹一下這四種方法的具體使用,感興趣的可以了解一下2023-05-05