動態數據源
在很多具體應用場景的時候,我們需要用到動態數據源的情況,比如多租戶的場景,系統登錄時需要根據用戶信息切換到用戶對應的數據庫。又比如業務A要訪問A數據庫,業務B要訪問B數據庫等,都可以使用動態數據源方案進行解決。接下來,我們就來講解如何實現動態數據源,以及在過程中剖析動態數據源背后的實現原理。
實現案例
本教程案例基於 Spring Boot + Mybatis + MySQL 實現。
數據庫設計
首先需要安裝好MySQL數據庫,新建數據庫 example,創建example表,用來測試數據源,SQL腳本如下:
CREATE TABLE `example` ( `pk` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `message` varchar(100) NOT NULL, `create_time` datetime NOT NULL COMMENT '創建時間', `modify_time` datetime DEFAULT NULL COMMENT '生效時間', PRIMARY KEY (`pk`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='測試用例表'
添加依賴
添加Spring Boot,Spring Aop,Mybatis,MySQL相關依賴。
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <!-- spring aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.8</version> </dependency>
自定義配置文件
新建自定義配置文件resource/config/mysql/db.properties,添加數據源:
#數據庫設置
spring.datasource.example.jdbc-url=jdbc:mysql://localhost:3306/example?characterEncoding=UTF-8
spring.datasource.example.username=root
spring.datasource.example.password=123456
spring.datasource.example.driver-class-name=com.mysql.jdbc.Driver
啟動類
啟動類添加 exclude = {DataSourceAutoConfiguration.class}, 以禁用數據源默認自動配置。
數據源默認自動配置會讀取 spring.datasource.* 的屬性創建數據源,所以要禁用以進行定制。
DynamicDatasourceApplication.java:
1 package com.main.example.dynamic.datasource; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 7 @SpringBootApplication(exclude = { 8 DataSourceAutoConfiguration.class 9 }) 10 public class DynamicDatasourceApplication { 11 12 public static void main(String[] args) { 13 SpringApplication.run(DynamicDatasourceApplication.class, args); 14 } 15 16 }
數據源配置類
創建一個數據源配置類,主要做以下幾件事情:
1. 配置 dao,model(bean),xml mapper文件的掃描路徑。
2. 注入數據源配置屬性,創建數據源。
3. 創建一個動態數據源,裝入數據源。
4. 將動態數據源設置到SQL會話工廠和事務管理器。
如此,當進行數據庫操作時,就會通過我們創建的動態數據源去獲取要操作的數據源了。
DbSourceConfig.java:
1 package com.main.example.config.dao; 2 3 import com.main.example.common.DataEnum; 4 import com.main.example.common.DynamicDataSource; 5 import org.mybatis.spring.SqlSessionFactoryBean; 6 import org.springframework.boot.context.properties.ConfigurationProperties; 7 import org.springframework.boot.jdbc.DataSourceBuilder; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration; 10 import org.springframework.context.annotation.PropertySource; 11 import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 12 import org.springframework.jdbc.datasource.DataSourceTransactionManager; 13 import org.springframework.transaction.PlatformTransactionManager; 14 15 import javax.sql.DataSource; 16 import java.util.HashMap; 17 import java.util.Map; 18 19 //數據庫配置統一在config/mysql/db.properties中 20 @Configuration 21 @PropertySource(value = "classpath:config/mysql/db.properties") 22 public class DbSourceConfig { 23 private String typeAliasesPackage = "com.main.example.bean.**.*"; 24 25 @Bean(name = "exampleDataSource") 26 @ConfigurationProperties(prefix = "spring.datasource.example") 27 public DataSource exampleDataSource() { 28 return DataSourceBuilder.create().build(); 29 } 30 31 /* 32 * 動態數據源 33 * dbMap中存放數據源名稱與數據源實例,數據源名稱存於DataEnum.DbSource中 34 * setDefaultTargetDataSource方法設置默認數據源 35 */ 36 @Bean(name = "dynamicDataSource") 37 public DataSource dynamicDataSource() { 38 DynamicDataSource dynamicDataSource = new DynamicDataSource(); 39 //配置多數據源 40 Map<Object, Object> dbMap = new HashMap(); 41 dbMap.put(DataEnum.DbSource.example.getName(), exampleDataSource()); 42 dynamicDataSource.setTargetDataSources(dbMap); 43 44 // 設置默認數據源 45 dynamicDataSource.setDefaultTargetDataSource(exampleDataSource()); 46 47 return dynamicDataSource; 48 } 49 50 /* 51 * 數據庫連接會話工廠 52 * 將動態數據源賦給工廠 53 * mapper存於resources/mapper目錄下 54 * 默認bean存於com.main.example.bean包或子包下,也可直接在mapper中指定 55 */ 56 @Bean(name = "sqlSessionFactory") 57 public SqlSessionFactoryBean sqlSessionFactory() throws Exception { 58 SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); 59 sqlSessionFactory.setDataSource(dynamicDataSource()); 60 sqlSessionFactory.setTypeAliasesPackage(typeAliasesPackage); //掃描bean 61 PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); 62 sqlSessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml")); // 掃描映射文件 63 64 return sqlSessionFactory; 65 } 66 67 @Bean 68 public PlatformTransactionManager transactionManager() { 69 // 配置事務管理, 使用事務時在方法頭部添加@Transactional注解即可 70 return new DataSourceTransactionManager(dynamicDataSource()); 71 } 72 }
動態數據源類
我們上一步把這個動態數據源設置到了SQL會話工廠和事務管理器,這樣在操作數據庫時就會通過動態數據源類來獲取要操作的數據源了。
動態數據源類集成了Spring提供的AbstractRoutingDataSource類,AbstractRoutingDataSource 中獲取數據源的方法就是 determineTargetDataSource,而此方法又通過 determineCurrentLookupKey 方法獲取查詢數據源的key。
所以如果我們需要動態切換數據源,就可以通過以下兩種方式定制:
1. 覆寫 determineCurrentLookupKey 方法
通過覆寫 determineCurrentLookupKey 方法,從一個自定義的 DbSourceContext.getDbSource() 獲取數據源key值,這樣在我們想動態切換數據源的時候,只要通過 DbSourceContext.setDbSource(key) 的方式就可以動態改變數據源了。這種方式要求在獲取數據源之前,要先初始化各個數據源到 DbSourceContext 中,我們案例就是采用這種方式實現的,所以要將數據源都事先初始化到DynamicDataSource 中。
2. 可以通過覆寫 determineTargetDataSource,因為數據源就是在這個方法創建並返回的,所以這種方式就比較自由了,支持到任何你希望的地方讀取數據源信息,只要最終返回一個 DataSource 的實現類即可。比如你可以到數據庫、本地文件、網絡接口等方式讀取到數據源信息然后返回相應的數據源對象就可以了。
DynamicDataSource.java:
1 package com.main.example.common; 2 3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4 5 public class DynamicDataSource extends AbstractRoutingDataSource { 6 7 @Override 8 protected Object determineCurrentLookupKey() { 9 return DbSourceContext.getDbSource(); 10 } 11 12 }
數據源上下文
動態數據源的切換主要是通過調用這個類的方法來完成的。在任何想要進行切換數據源的時候都可以通過調用這個類的方法實現切換。比如系統登錄時,根據用戶信息調用這個類的數據源切換方法切換到用戶對應的數據庫。完整代碼如下:
DbSourceContext.java:
1 package com.main.example.common; 2 3 import org.apache.log4j.Logger; 4 5 public class DbSourceContext { 6 private static Logger logger = Logger.getLogger(DbSourceContext.class); 7 8 private static final ThreadLocal<String> dbContext = new ThreadLocal<String>(); 9 10 public static void setDbSource(String source) { 11 logger.debug("set source ====>" + source); 12 dbContext.set(source); 13 } 14 15 public static String getDbSource() { 16 logger.debug("get source ====>" + dbContext.get()); 17 return dbContext.get(); 18 } 19 20 public static void clearDbSource() { 21 dbContext.remove(); 22 } 23 }
注解式數據源
到這里,在任何想要動態切換數據源的時候,只要調用DbSourceContext.setDbSource(key) 就可以完成了。
接下來我們實現通過注解的方式來進行數據源的切換,原理就是添加注解(如@DbSource(value="example")),然后實現注解切面進行數據源切換。
創建一個動態數據源注解,擁有一個value值,用於標識要切換的數據源的key。
DbSource.java:
1 package com.main.example.config.dao; 2 3 import java.lang.annotation.*; 4 5 /** 6 * 動態數據源注解 7 * @author 8 * @date April 12, 2019 9 */ 10 @Target({ElementType.METHOD, ElementType.TYPE}) 11 @Retention(RetentionPolicy.RUNTIME) 12 @Documented 13 public @interface DbSource { 14 /** 15 * 數據源key值 16 * @return 17 */ 18 String value(); 19 }
創建一個AOP切面,攔截帶 @DataSource 注解的方法,在方法執行前切換至目標數據源,執行完成后恢復到默認數據源。
DynamicDataSourceAspect.java:
1 package com.main.example.config.dao; 2 3 import com.main.example.common.DbSourceContext; 4 import org.apache.log4j.Logger; 5 import org.aspectj.lang.JoinPoint; 6 import org.aspectj.lang.annotation.After; 7 import org.aspectj.lang.annotation.Aspect; 8 import org.aspectj.lang.annotation.Before; 9 import org.springframework.core.annotation.Order; 10 import org.springframework.stereotype.Component; 11 12 /** 13 * 動態數據源切換處理器 14 * @author linzhibao 15 * @date April 12, 2019 16 */ 17 @Aspect 18 @Order(-1) // 該切面應當先於 @Transactional 執行 19 @Component 20 public class DynamicDataSourceAspect { 21 private static Logger logger = Logger.getLogger(DynamicDataSourceAspect.class); 22 /** 23 * 切換數據源 24 * @param point 25 * @param dbSource 26 */ 27 //@Before("@annotation(dbSource)") 注解在對應方法,攔截有@DbSource的方法 28 //注解在類對象,攔截有@DbSource類下所有的方法 29 @Before("@within(dbSource)") 30 public void switchDataSource(JoinPoint point, DbSource dbSource) { 31 // 切換數據源 32 DbSourceContext.setDbSource(dbSource.value()); 33 } 34 35 /** 36 * 重置數據源 37 * @param point 38 * @param dbSource 39 */ 40 //注解在類對象,攔截有@DbSource類下所有的方法 41 @After("@within(dbSource)") 42 public void restoreDataSource(JoinPoint point, DbSource dbSource) { 43 // 將數據源置為默認數據源 44 DbSourceContext.clearDbSource(); 45 } 46 }
到這里,動態數據源相關的處理代碼就完成了。
編寫用戶業務代碼
接下來編寫用戶查詢業務代碼,用來進行測試,Dao層只需添加一個查詢接口即可。
ExampleDao.java:
1 package com.main.example.dao; 2 3 import com.main.example.common.DataEnum; 4 import com.main.example.config.dao.DbSource; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.stereotype.Component; 7 8 import javax.annotation.Resource; 9 import java.util.List; 10 11 @Component("exampleDao") 12 //切換數據源注解,以DataEnum.DbSource中的值為准 13 @DbSource("example") 14 public class ExampleDao extends DaoBase { 15 private static final String MAPPER_NAME_SPACE = "com.main.example.dao.ExampleMapper"; 16 17 public List<String> selectAllMessages() { 18 return selectList(MAPPER_NAME_SPACE, "selectAllMessages"); 19 } 20 }
Controler代碼:
TestExampleDao.java:
1 package com.main.example.dao; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RestController; 6 7 import java.util.ArrayList; 8 import java.util.List; 9 10 @RestController 11 public class TestExampleDao { 12 @Autowired 13 ExampleDao exampleDao; 14 15 @RequestMapping(value = "/test/example") 16 public List<String> selectAllMessages() { 17 try { 18 List<String> ldata = exampleDao.selectAllMessages(); 19 if(ldata == null){System.out.println("*********it is null.***********");return null;} 20 for(String d : ldata) { 21 System.out.println(d); 22 } 23 return ldata; 24 }catch(Exception e) { 25 e.printStackTrace(); 26 } 27 28 return new ArrayList<>(); 29 } 30 }
ExampleMapper.xml代碼:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.main.example.dao.ExampleMapper"> <select id="selectAllMessages" resultType="java.lang.String"> SELECT message FROM example </select> </mapper>
測試效果
啟動系統,訪問 http://localhost:80/test/example,分別測試兩個接口,成功返回數據。
可能遇到的問題
1.報錯:java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName
原因:
spring boot從1.X升級到2.X版本之后,一些配置及用法有了變化,如果不小心就會碰到“jdbcUrl is required with driverClassName.”的錯誤
解決方法:
在1.0 配置數據源的過程中主要是寫成:spring.datasource.url 和spring.datasource.driverClassName。
而在2.0升級之后需要變更成:spring.datasource.jdbc-url和spring.datasource.driver-class-name即可解決!
2.自定義配置文件
自定義配置文件需要在指定配置類上加上@PropertySource標簽,例如:
@PropertySource(value = "classpath:config/mysql/db.properties")
若是作用於配置類中的方法,則在方法上加上@ConfigurationProperties,例如:
@ConfigurationProperties(prefix = "spring.datasource.example")
配置項前綴為spring.datasource.example
若是作用於配置類上,則在類上加上@ConfigurationProperties(同上),並且在啟動類上加上@EnableConfigurationProperties(XXX.class)
3.多數據源
需要在啟動類上取消自動裝載數據源,如:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
附:
如果想在數據層數據層直接使用mapper,只需要在對應的包下建立和*mapper.xml中namespace對應的類,然后在該類上加上@Mapper標注,或者在程序初始時使用@MapperScan掃描全mapper包
轉載請注明出處:https://www.cnblogs.com/fnlingnzb-learner/p/10710145.html