SpringBoot自定義動態(tài)數(shù)據(jù)源的流程步驟
1. 原理
動態(tài)數(shù)據(jù)源,本質(zhì)上是把多個數(shù)據(jù)源存儲在一個 Map
中,當需要使用某一個數(shù)據(jù)源時,使用 key
獲取指定數(shù)據(jù)源進行處理。而在 Spring
中已提供了抽象類 AbstractRoutingDataSource
來實現(xiàn)此功能,繼承 AbstractRoutingDataSource
類并覆寫其 determineCurrentLookupKey()
方法監(jiān)聽獲取 key
即可,該方法只需要返回數(shù)據(jù)源 key
即可,也就是存放數(shù)據(jù)源的 Map
的 key
。
因此,我們在實現(xiàn)動態(tài)數(shù)據(jù)源的,只需要繼承它,實現(xiàn)自己的獲取數(shù)據(jù)源邏輯即可。AbstractRoutingDataSource
頂級繼承了 DataSource
,所以它也是可以做為數(shù)據(jù)源對象,因此項目中使用它作為主數(shù)據(jù)源。
1.1. AbstractRoutingDataSource 源碼解析
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { // 目標數(shù)據(jù)源 map 集合,存儲將要切換的多數(shù)據(jù)源 bean 信息,可以通過 setTargetDataSource(Map<Object, Object> mp) 設置 @Nullable private Map<Object, Object> targetDataSources; // 未指定數(shù)據(jù)源時的默認數(shù)據(jù)源對象,可以通過 setDefaultTargetDataSouce(Object obj) 設置 @Nullable private Object defaultTargetDataSource; ... // 數(shù)據(jù)源查找接口,通過該接口的 getDataSource(String dataSourceName) 獲取數(shù)據(jù)源信息 private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); //解析 targetDataSources 之后的 DataSource 的 map 集合 @Nullable private Map<Object, DataSource> resolvedDataSources; @Nullable private DataSource resolvedDefaultDataSource; //將 targetDataSources 的內(nèi)容轉(zhuǎn)化一下放到 resolvedDataSources 中,將 defaultTargetDataSource 轉(zhuǎn)為 DataSource 賦值給 resolvedDefaultDataSource public void afterPropertiesSet() { //如果目標數(shù)據(jù)源為空,會拋出異常,在系統(tǒng)配置時應至少傳入一個數(shù)據(jù)源 if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } else { //初始化 resolvedDataSources 的大小 this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size()); //遍歷目標數(shù)據(jù)源信息 map 集合,對其中的 key,value 進行解析 this.targetDataSources.forEach((key, value) -> { // resolveSpecifiedLookupKey 方法沒有做任何處理,只是將 key 繼續(xù)返回 Object lookupKey = this.resolveSpecifiedLookupKey(key); // 將目標數(shù)據(jù)源 map 集合中的 value 值(Druid 數(shù)據(jù)源信息)轉(zhuǎn)為 DataSource 類型 DataSource dataSource = this.resolveSpecifiedDataSource(value); // 將解析之后的 key,value 放入 resolvedDataSources 集合中 this.resolvedDataSources.put(lookupKey, dataSource); }); if (this.defaultTargetDataSource != null) { // 將默認目標數(shù)據(jù)源信息解析并賦值給 resolvedDefaultDataSource this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource); } } } protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { if (dataSource instanceof DataSource) { return (DataSource)dataSource; } else if (dataSource instanceof String) { return this.dataSourceLookup.getDataSource((String)dataSource); } else { throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); } } // 因為 AbstractRoutingDataSource 繼承 AbstractDataSource,而 AbstractDataSource 實現(xiàn)了 DataSource 接口,所有存在獲取數(shù)據(jù)源連接的方法 public Connection getConnection() throws SQLException { return this.determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return this.determineTargetDataSource().getConnection(username, password); } // 最重要的一個方法,也是 DynamicDataSource 需要實現(xiàn)的方法 protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); // 調(diào)用實現(xiàn)類中重寫的 determineCurrentLookupKey 方法拿到當前線程要使用的數(shù)據(jù)源的名稱 Object lookupKey = this.determineCurrentLookupKey(); // 去解析之后的數(shù)據(jù)源信息集合中查詢該數(shù)據(jù)源是否存在,如果沒有拿到則使用默認數(shù)據(jù)源 resolvedDefaultDataSource 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; } } @Nullable protected abstract Object determineCurrentLookupKey(); }
1.2. 關鍵類說明
忽略掉 controller
/service
/entity
/mapper
/xml
介紹。
application.yml
:數(shù)據(jù)源配置文件。但是如果數(shù)據(jù)源比較多的話,根據(jù)實際使用,最佳的配置方式還是獨立配置比較好。DynamicDataSourceRegister
:動態(tài)數(shù)據(jù)源注冊配置文件DynamicDataSource
:動態(tài)數(shù)據(jù)源配置類,繼承自AbstractRoutingDataSource
TargetDataSource
:動態(tài)數(shù)據(jù)源注解,切換當前線程的數(shù)據(jù)源DynamicDataSourceAspect
:動態(tài)數(shù)據(jù)源設置切面,環(huán)繞通知,切換當前線程數(shù)據(jù)源,方法注解優(yōu)先DynamicDataSourceContextHolder
:動態(tài)數(shù)據(jù)源上下文管理器,保存當前數(shù)據(jù)源的key
,默認數(shù)據(jù)源名,所有數(shù)據(jù)源key
1.3. 開發(fā)流程
- 添加配置文件,設置默認數(shù)據(jù)源配置,和其他數(shù)據(jù)源配置
- 編寫
DynamicDataSource
類,繼承AbstractRoutingDataSource
類,并實現(xiàn)determineCurrentLookupKey()
方法 - 編寫
DynamicDataSourceHolder
上下文管理類,管理當前線程的使用的數(shù)據(jù)源,及所有數(shù)據(jù)源的key
; - 編寫
DynamicDataSourceRegister
類通過讀取配置文件動態(tài)注冊多數(shù)據(jù)源,并在啟動類上導入(@Import
)該類 - 自定義數(shù)據(jù)源切換注解
TargetDataSource
,并實現(xiàn)相應的切面,環(huán)繞通知切換當前線程數(shù)據(jù)源,注解優(yōu)先級(DynamicDataSourceHolder.setDynamicDataSourceKey()
>Method
>Class
)
2. 實現(xiàn)
2.1. 引入 Maven 依賴
<!-- web 模塊依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- spring 核心 aop 模塊依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- Druid 數(shù)據(jù)源連接池依賴 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <!-- mybatis 依賴 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!-- mysql驅(qū)動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.24</version> </dependency> <!-- lombok 模塊依賴 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.10.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
2.2. application.yml 配置文件
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding-utf8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root custom: datasource: names: ds1,ds2 ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/content_center?useUnicode username: root password: root ds2: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/trade?useUnicode username: root password: root
2.3. 創(chuàng)建 DynamicDataSource 繼承 AbstractRoutingDataSource 類
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; /** * @Description: 繼承Spring AbstractRoutingDataSource 實現(xiàn)路由切換 */ @Data @NoArgsConstructor @AllArgsConstructor public class DynamicDataSource extends AbstractRoutingDataSource { /** * 決定當前線程使用哪種數(shù)據(jù)源 * @return 數(shù)據(jù)源 key */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); } }
2.4. 編寫 DynamicDataSourceHolder 類,管理 DynamicDataSource 上下文
import java.util.ArrayList; import java.util.List; /** * @Description: 動態(tài)數(shù)據(jù)源上下文管理 */ public class DynamicDataSourceHolder { // 存放當前線程使用的數(shù)據(jù)源類型信息 private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<String>(); // 存放數(shù)據(jù)源 key private static final List<String> DATASOURCE_KEYS = new ArrayList<String>(); // 默認數(shù)據(jù)源 key public static final String DEFAULT_DATESOURCE_KEY = "master"; //設置數(shù)據(jù)源 public static void setDynamicDataSourceType(String key) { DYNAMIC_DATASOURCE_KEY.set(key); } //獲取數(shù)據(jù)源 public static String getDynamicDataSourceType() { return DYNAMIC_DATASOURCE_KEY.get(); } //清除數(shù)據(jù)源 public static void removeDynamicDataSourceType() { DYNAMIC_DATASOURCE_KEY.remove(); } public static void addDataSourceKey(String key) { DATASOURCE_KEYS.add(key) } /** * 判斷指定 key 當前是否存在 * * @param key * @return boolean */ public static boolean containsDataSource(String key){ return DATASOURCE_KEYS.contains(key); } }
2.5. 編寫 DynamicDataSourceRegister 讀取配置文件注冊多數(shù)據(jù)源
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotationMetadata; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.StringUtils; import java.util.Objects; /** * @Description: 注冊動態(tài)數(shù)據(jù)源 * 初始化數(shù)據(jù)源和提供了執(zhí)行動態(tài)切換數(shù)據(jù)源的工具類 * EnvironmentAware(獲取配置文件配置的屬性值) */ public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware { private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSourceRegister.class); // 指定默認數(shù)據(jù)源類型 (springboot2.0 默認數(shù)據(jù)源是 hikari 如何想使用其他數(shù)據(jù)源可以自己配置) // private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource"; private static final String DEFAULT_DATASOURCE_TYPE = "com.alibaba.druid.pool.DruidDataSource"; // 默認數(shù)據(jù)源 private DataSource defaultDataSource; // 用戶自定義數(shù)據(jù)源 private Map<String, DataSource> customDataSources = new HashMap<>(); /** * 加載多數(shù)據(jù)源配置 * @param env 當前環(huán)境 */ @Override public void setEnvironment(Environment env) { initDefaultDataSource(env); initCustomDataSources(env); } /** * 初始化主數(shù)據(jù)源 * @param env */ private void initDefaultDataSource(Environment env) { // 讀取主數(shù)據(jù)源 Map<String, Object> dsMap = new HashMap<>(); dsMap.put("type", env.getProperty("spring.datasource.type", DEFAULT_DATASOURCE_TYPE)); dsMap.put("driver", env.getProperty("spring.datasource.driver-class-name")); dsMap.put("url", env.getProperty("spring.datasource.url")); dsMap.put("username", env.getProperty("spring.datasource.username")); dsMap.put("password", env.getProperty("spring.datasource.password")); defaultDataSource = buildDataSource(dsMap); } /** * 初始化更多數(shù)據(jù)源 * @param env */ private void initCustomDataSources(Environment env) { // 讀取配置文件獲取更多數(shù)據(jù)源 String dsPrefixs = env.getProperty("custom.datasource.names"); if (!StringUtils.isBlank(dsPrefixs)) { for (String dsPrefix : dsPrefixs.split(",")) { dsPrefix = fsPrefix.trim() if (!StringUtils.isBlank(dsPrefix)) { Map<String, Object> dsMap = new HashMap<>(); dsMap.put("type", env.getProperty("custom.datasource." + dsPrefix + ".type", DEFAULT_DATASOURCE_TYPE)); dsMap.put("driver", env.getProperty("custom.datasource." + dsPrefix + ".driver-class-name")); dsMap.put("url", env.getProperty("custom.datasource." + dsPrefix + ".url")); dsMap.put("username", env.getProperty("custom.datasource." + dsPrefix + ".username")); dsMap.put("password", env.getProperty("custom.datasource." + dsPrefix + ".password")); DataSource ds = buildDataSource(dsMap); customDataSources.put(dsPrefix, ds); } } } } @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); // 將主數(shù)據(jù)源添加到更多數(shù)據(jù)源中 targetDataSources.put(DynamicDataSourceHolder.DEFAULT_DATASOURCE_KEY, defaultDataSource); DynamicDataSourceHolder.addDataSourceKey(DynamicDataSourceHolder.DEFAULT_DATASOURCE_KEY); // 添加更多數(shù)據(jù)源 targetDataSources.putAll(customDataSources); for (String key : customDataSources.keySet()) { DynamicDataSourceContextHolder.addDataSourceKey(key); } // 創(chuàng)建 DynamicDataSource GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(DynamicDataSource.class); beanDefinition.setSynthetic(true); MutablePropertyValues mpv = beanDefinition.getPropertyValues(); mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource); mpv.addPropertyValue("targetDataSources", targetDataSources); registry.registerBeanDefinition("dataSource", beanDefinition); // 注冊到 Spring 容器中 LOGGER.info("Dynamic DataSource Registry"); } /** * 創(chuàng)建 DataSource * @param dsMap 數(shù)據(jù)庫配置參數(shù) * @return DataSource */ public DataSource buildDataSource(Map<String, Object> dsMap) { try { Object type = dsMap.get("type"); if (type == null) type = DEFAULT_DATASOURCE_TYPE;// 默認DataSource Class<? extends DataSource> dataSourceType = (Class<? extends DataSource>)Class.forName((String)type); String driverClassName = String.valueOf(dsMap.get("driver")); String url = String.valueOf(dsMap.get("url")); String username = String.valueOf(dsMap.get("username")); String password = String.valueOf(dsMap.get("password")); // 自定義 DataSource 配置 DataSourceBuilder<? extends DataSource> factory = DataSourceBuilder.create() .driverClassName(driverClassName) .url(url) .username(username) .password(password) .type(dataSourceType); return factory.build(); }catch (ClassNotFoundException e) { e.printStackTrace(); } } }
2.6. 在啟動器類上添加 @Import,導入 register 類
// 注冊動態(tài)多數(shù)據(jù)源 @Import({ DynamicDataSourceRegister.class }) @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
2.7. 自定義注解 @TargetDataSource
/** * 自定義多數(shù)據(jù)源切換注解 * 優(yōu)先級:DynamicDataSourceHolder.setDynamicDataSourceKey() > Method > Class */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface DataSource { /** * 切換數(shù)據(jù)源名稱 */ public String value() default DynamicDataSourceHolder.DEFAULT_DATESOURCE_KEY; }
2.8. 定義切面攔截 @TargetDataSource
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.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Objects; @Aspect // 保證在 @Transactional 等注解前面執(zhí)行 @Order(-1) @Component public class DataSourceAspect { // 設置 DataSource 注解的切點表達式 @Pointcut("@annotation(com.ayi.config.datasource.DynamicDataSource)") public void dynamicDataSourcePointCut(){ } //環(huán)繞通知 @Around("dynamicDataSourcePointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ String key = getDefineAnnotation(joinPoint).value(); if (!DynamicDataSourceHolder.containsDataSource(key)) { LOGGER.error("數(shù)據(jù)源[{}]不存在,使用默認數(shù)據(jù)源[{}]", key, DynamicDataSourceHolder.DEFAULT_DATESOURCE_KEY) key = DynamicDataSourceHolder.DEFAULT_DATESOURCE_KEY; } DynamicDataSourceHolder.setDynamicDataSourceKey(key); try { return joinPoint.proceed(); } finally { DynamicDataSourceHolder.removeDynamicDataSourceKey(); } } /** * 先判斷方法的注解,后判斷類的注解,以方法的注解為準 * @param joinPoint 切點 * @return TargetDataSource */ private TargetDataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){ MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); TargetDataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(TargetDataSource.class); if (Objects.nonNull(methodSignature)) { return dataSourceAnnotation; } else { Class<?> dsClass = joinPoint.getTarget().getClass(); return dsClass.getAnnotation(TargetDataSource.class); } } }
以上就是SpringBoot自定義動態(tài)數(shù)據(jù)源的流程步驟的詳細內(nèi)容,更多關于SpringBoot動態(tài)數(shù)據(jù)源的資料請關注腳本之家其它相關文章!
相關文章
Mybatis實現(xiàn)插入數(shù)據(jù)后返回主鍵過程解析
這篇文章主要介紹了Mybatis實現(xiàn)插入數(shù)據(jù)后返回主鍵過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06MyBatis中example.createCriteria()方法的具體使用
本文詳細介紹了MyBatis的Example工具的使用方法,包括鏈式調(diào)用指定字段、設置查詢條件、支持多種查詢方式等,還介紹了mapper的crud方法、and/or方法的使用,以及如何進行多條件和多重條件查詢,感興趣的可以了解一下2024-10-10Java多線程案例實戰(zhàn)之定時器的實現(xiàn)
在Java中可以使用多線程和定時器來實現(xiàn)定時任務,下面這篇文章主要給大家介紹了關于Java多線程案例之定時器實現(xiàn)的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-01-01