Springboot整合Mybatis實現多數據源動態切換
1 業務背景
最近一個項目中需要在oracle數據庫中讀取用戶信息,需要在mysql中讀取業務數據。
2 解決方案
- 多數據源配置
在比較大型的項目中,數據庫可能會分佈在多臺服務器上,例如有若干個數據庫服務是專門存放日誌數據的,又有若干個數據庫服務是專門存放業務數據、讀寫分離的等等....這時候應用程序如果需要對這兩種類型的數據進行處理的話,就需要配置多數據源了。
- 微服務化
在如今微服務大行其道的今天,沒有啥是爲服務解決不了的,既然需要在不同的數據庫讀取信息,那麼我們就可以將業務拆分爲二,獨立一個服務出來專門負責oracle數據庫的連接查詢,獨立一個服務負責mysql數據庫的業務處理。再新建一個工程調用這兩個服務問題就迎刃而解。
由於該項目是存在已久的老項目,目前不具備微服務化得能力而且時間也不允許。因此只能退而求其次在項目中引進多數據源。
達到按照需求動態切換的目的。
3 具體實現
pom文件配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-mybatis-multiple-datasource</artifactId>
<packaging>jar</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-mybatis-multiple-datasource</name>
<description>Demo Multiple Datasource for Spring Boot</description>
<parent>
<groupId>com.along</groupId>
<artifactId>spring-boot-all</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 分頁插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
<!-- mybatis-generator-core 反向生成java代碼-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<!-- alibaba的druid數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--mybatis逆向工程maven插件-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<!--允許移動生成的文件-->
<verbose>true</verbose>
<!--允許覆蓋生成的文件-->
<overwrite>true</overwrite>
<!--配置文件的路徑 默認resources目錄下-->
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
</configuration>
<!--插件依賴的jar包-->
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
application.properties文件
spring.application.name=springboot-datasource
server.port=8081
## mysql database
spring.datasource.local.url=****
spring.datasource.local.username=****
spring.datasource.local.password=****
spring.datasource.local.driver-class-name=com.mysql.jdbc.Driver
## oracle database
spring.datasource.oracle.url=****
spring.datasource.oracle.username=****
spring.datasource.oracle.password****
spring.datasource.oracle.driver-class-name=****
實際配置以實際爲準,以上僅供參考:
- 基於分包方式實現
配置mysql數據庫的datasource
package com.lb.api.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
@Configuration
//開啓事務管理
@MapperScan(basePackages = "com.lb.api.dao.local", sqlSessionFactoryRef = "test1SqlSessionFactory")
public class LocalDataSourceConfig {
//這裏必須要要加 @Qualifier(value = "localDatasource")註解
//否則Spring將不會知道用哪個Bean
@Bean(name = "localDatasource")
@Qualifier(value = "localDatasource")
@ConfigurationProperties(prefix = "spring.datasource.local")
public DataSource localDatasource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "test1SqlSessionFactory")
// 表示這個數據源是默認數據源
@Primary
// @Qualifier表示查找Spring容器中名字爲test1DataSource的對象
public SqlSessionFactory test1SqlSessionFactory(@Qualifier("localDatasource") DataSource datasource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setMapperLocations(
// 設置mybatis的xml所在位置
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/test01/*.xml"));
return bean.getObject();
}
@Bean("test1SqlSessionTemplate")
// 表示這個數據源是默認數據源
@Primary
public SqlSessionTemplate test1sqlsessiontemplate(
@Qualifier("test1SqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
}
1:@MapperScan(basePackages = "com.lb.api.dao.local", sqlSessionFactoryRef = "test1SqlSessionFactory")
其中basePackages配置是你需要連接mysql數據庫的mapper文件存放的路徑。
2:@ConfigurationProperties(prefix = "spring.datasource.local")
prefix 配置讀取以local開頭的數據庫配置屬性
3:@Primary一定要加,並且只能加在一個datasource上,因爲它代表默認配置,加在你需要設置的默認datasource上。
4: new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/test02/*.xml"));
配置你需要掃描的mapper.xml文件的路徑,記得不同數據源需要區分開來。
配置oracle數據庫的datasource
package com.lb.api.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
@Configuration
//配置mybatis的接口類放的地方
@MapperScan(basePackages = "com.lb.api.dao.oracle", sqlSessionFactoryRef = "test2SqlSessionFactory")
public class OracleDatasourceConfig {
@Bean(name = "oracleDatasource")
@Qualifier(value = "oracleDatasource")
@ConfigurationProperties(prefix = "spring.datasource.oracle")
public DataSource oracleDatasource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "test2SqlSessionFactory")
// 表示這個數據源是默認數據源
// @Qualifier表示查找Spring容器中名字爲test1DataSource的對象
public SqlSessionFactory test1SqlSessionFactory(@Qualifier("oracleDatasource") DataSource datasource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setMapperLocations(
// 設置mybatis的xml所在位置
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/test02/*.xml"));
return bean.getObject();
}
@Bean("test2SqlSessionTemplate")
// 表示這個數據源是默認數據源
public SqlSessionTemplate test1sqlsessiontemplate(
@Qualifier("test2SqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
}
至此一個雙數據源的服務就搭建好了,當你調用相應mapper文件裏的數據庫操作時會調用不同的數據庫。
- 基於AOP方式的動態切換
1. 數據源配置類 MultipleDataSourceConfig.java
package com.lb.api.config;
import java.util.HashMap;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class DataSourceConfig {
@Bean(name = "localDatasource")
@Qualifier(value = "localDatasource")
@ConfigurationProperties(prefix = "spring.datasource.local")
public DataSource localDatasource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "oracleDatasource")
@Qualifier(value = "oracleDatasource")
@ConfigurationProperties(prefix = "spring.datasource.oracle")
public DataSource oracleDatasource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "sqlSessionFactory")
// 表示這個數據源是默認數據源
// @Qualifier表示查找Spring容器中名字爲test1DataSource的對象
public SqlSessionFactory sqlSessionFactory()
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource());
bean.setMapperLocations(
// 設置mybatis的xml所在位置
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
return bean.getObject();
}
@Bean("sqlSessionTemplate")
// 表示這個數據源是默認數據源
public SqlSessionTemplate sqlsessiontemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//配置默認數據源
dynamicDataSource.setDefaultTargetDataSource(localDatasource());
//配置多數據源
HashMap<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(ContextConst.DataSourceType.PRIMARY.name(), oracleDatasource());
dataSourceMap.put(ContextConst.DataSourceType.LOCAL.name(), localDatasource());
dynamicDataSource.setTargetDataSources(dataSourceMap); // 該方法是AbstractRoutingDataSource的方法
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager bfTransactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
// @Bean
// public PlatformTransactionManager bfscrmTransactionManager(@Qualifier("oracleDatasource")DataSource oracleDatasource) {
// return new DataSourceTransactionManager(oracleDatasource);
// }
}
2. 數據源持有類 DataSourceContextHolder.java
/**
* 數據源持有類
*/
public class DataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dbType){
logger.info("切換到[{}]數據源",dbType);
contextHolder.set(dbType);
}
public static String getDataSource(){
return contextHolder.get();
}
public static void clearDataSource(){
contextHolder.remove();
}
}
3. 數據源路由實現類 DynamicDataSource.java
/**
* 數據源路由實現類
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
String dataSource = DataSourceContextHolder.getDataSource();
if (dataSource == null) {
logger.info("當前數據源爲[primary]");
} else {
logger.info("當前數據源爲{}", dataSource);
}
return dataSource;
}
}
4. 自定義切換數據源的註解
/**
* 切換數據源的註解
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
ContextConst.DataSourceType value() default ContextConst.DataSourceType.PRIMARY;
}
5. 數據源枚舉類
/**
* 上下文常量
*/
public interface ContextConst {
/**
* 數據源枚舉
*/
enum DataSourceType {
PRIMARY, LOCAL, PROD, TEST
}
}
6. 定義切換數據源的切面,爲註解服務
package com.lb.api.config;
import java.lang.reflect.Method;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 切換數據源的切面
*/
@Component
@Aspect
@Order(1) //這是關鍵,要讓該切面調用先於AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {
@Before("execution(* com.lb.api.service..*.*(..))")
public void before(JoinPoint point) {
try {
DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class);
String methodName = point.getSignature().getName();
Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
DataSource methodAnnotation = method.getAnnotation(DataSource.class);
methodAnnotation = methodAnnotation == null ? annotationOfClass : methodAnnotation;
ContextConst.DataSourceType dataSourceType = methodAnnotation != null
&& methodAnnotation.value() != null ? methodAnnotation.value() : ContextConst.DataSourceType.PRIMARY;
DataSourceContextHolder.setDataSource(dataSourceType.name());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
@After("execution(* com.lb.api.service..*.*(..))")
public void after(JoinPoint point) {
DataSourceContextHolder.clearDataSource();
}
}
7. 修改啓動類
package com.lb.api;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.lb.api.service.DataSourceService;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan(basePackages = "com.lb.api.dao")
@RestController
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
@Autowired
private DataSourceService dataSourceService;
@RequestMapping(value="/test")
public void test(){
dataSourceService.test01();
dataSourceService.test02();
}
}
8 使用
在方法上通過註解@DataSource指定該方法所用的數據源,如果沒有使用註解指定則使用默認數據源
下面是在service實現類中的應用:
package com.lb.api.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.lb.api.config.ContextConst;
import com.lb.api.config.DataSource;
import com.lb.api.dao.LivesubscriptionMapper;
import com.lb.api.dao.TOperHisMapper;
@Service
public class DataSourceService {
@Autowired
private LivesubscriptionMapper livesubscriptionMapper;
@Autowired
private TOperHisMapper tOperHisMapper;
@DataSource(ContextConst.DataSourceType.LOCAL) // 指定該方法使用prod數據源
public void test01() {
System.out.println(livesubscriptionMapper.selectByExample().size());
}
@DataSource(ContextConst.DataSourceType.PRIMARY) // 指定該方法使用prod數據源
public void test02() {
System.out.println(tOperHisMapper.selectByExample().size());
}
}
訪問鏈接localhost:8081/test
2019-10-09 16:36:35.777 INFO 14752 --- [nio-8081-exec-7] c.lb.api.config.DataSourceContextHolder : 切換到[LOCAL]數據源
2019-10-09 16:36:35.785 INFO 14752 --- [nio-8081-exec-7] com.lb.api.config.DynamicDataSource : 當前數據源爲LOCAL
83
2019-10-09 16:36:35.838 INFO 14752 --- [nio-8081-exec-7] c.lb.api.config.DataSourceContextHolder : 切換到[PRIMARY]數據源
2019-10-09 16:36:35.838 INFO 14752 --- [nio-8081-exec-7] com.lb.api.config.DynamicDataSource : 當前數據源爲PRIMARY
0
達到了動態切換的目的。