spring、spring boot中配置多数据源

时间:2022-09-03 18:01:25

在项目开发的过程中,有时我们有这样的需求,需要去调用别的系统中的数据,那么这个时候系统中就存在多个数据源了,那么我们如何来解决程序在运行的过程中到底是使用的那个数据源呢?

假设我们系统中存在2个数据源 mysql 和 oracle. 系统中存在 2个方法  methodA 和 methodB ,其中methodA是是需要去调用mysql、methodB是需要使用到oracle,那么在我们调用methodA或methodB时是如何知道使用的是mysql还是oracle呢? 我的做法是  使用Spring 的 动态数据源路由来解决这个问题。

需求: 系统中要显示一些图表信息,但是图表的数据是来自别的数据库,不是自己系统使用的数据库,如何来解决这个问题。

解决方案:在程序运行的过程中动态的去决定使用哪种数据源,借助Spring 的抽象数据源路由来解决。

前置条件:

1、@Primary 表示在程序中存在同一种类型的bean有多个时,默认使用有@Primay 注解标注的

2、当产生了Connection后,事务可能就会开启了

3、如果要获取到一个Connection,那么我们需要在事务开启前知道使用的是那个数据源,如果我们要写切面去判断使用那个数据源,那么一定要在事务切面之前

4、如果一个变量要在程序的任何地方都可以获取到,那么可以使用ThreadLocal来存放这个变量,如果存放在static类型的变量中,那么会存在线程安全问题。

解决思路:

1、在Spring 中提供了一个类  AbstractRoutingDataSource 这个类中有一个determineCurrentLookupKey() 方法,在这个方法中返回一个 key ,那么Spring 就知道使用那个数据源。那么Spring 怎么知道这个你返回的 key 是什么意思呢? 那么在这个类中必然有个方法 可以让 key 和 数据源 进行关联即 setTargetDataSources(Map<Object, Object> targetDataSources) 方法,在这个方法中的参数中传递一个map ,可以让map 的 key  为 determineCurrentLookupKey() 需要的key ,值为具体的数据源。

2、那么我们什么时候需要设置 determineCurrentLookupKey() 方法的key呢?那么肯定是那个方法需要切换数据源,即此时我们需要向ThreadLocal中设置当前数据源的key,那么假如我们有很多方法需要切换数据源,难道每个方法都要自己手动向ThreadLocal中插入值吗?这个代码太冗余了,那么我们此时就可以自定义一个注解,然后写一个切面凡是带这个注解的方法都进行拦截,然后再在这个切面中设置数据源需要的key.

3、那么我们的切面什么时候执行呢?我们知道事务是有Connection开启的,如果我们的切面在事务之后执行,那么什么意义也没有,即数据源切换失败。所以我们自己写的切面需要在Spring 的事务切面之前执行,使用@Order注解执行切面的执行顺序,注解里面的值越小越先执行。

4、我们知道我们的事务开启需要数据源,或Jpa操作数据库也需要数据源,那么我们应该将那个数据源作为主数据源呢?是我们系统的数据源还是第三方系统的数据源,这个肯定不是,应该有程序运行时动态决策,因此就需要一个 实现了 AbstractRoutingDataSource 的类作为主数据源。

具体步骤:

一、引入pom.xml文件

<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> <groupId>com.huan.springboot</groupId>
<artifactId>springboot_20_multi_datasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging> <name>springboot_20_multi_datasource</name>
<url>http://maven.apache.org</url> <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.3.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</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-data-jpa</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc14</artifactId>
<version>10.2.0.4.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

 二、配置2个数据源

1、配置application.yml文件,向里面增加2个不同数据源的配置

spring.datasource.mysql.url=jdbc:mysql://localhost/information_schema?useUnicode=true&characterEncoding=utf-8
spring.datasource.mysql.username=root
spring.datasource.mysql.password=root
spring.datasource.mysql.driver-class-name=com.mysql.jdbc.Driver spring.datasource.oracle.url=jdbc:oracle:thin:@127.0.0.1:1521:orcl
spring.datasource.oracle.username=system
spring.datasource.oracle.password=admin
spring.datasource.oracle.driver-class-name=oracle.jdbc.driver.OracleDriver spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy

2、编写 MultiDataSourceConfig 配置文件,配置这2个数据源

package com.huan.springboot.config;

import javax.sql.DataSource;

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; /**
* 多数据源配置
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午8:47:30
*/
@Configuration
public class MultiDataSourceConfig { @Bean
@ConfigurationProperties("spring.datasource.mysql")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();
} @Bean
@ConfigurationProperties("spring.datasource.oracle")
public DataSource oracleDataSource() {
return DataSourceBuilder.create().build();
} }

 三、编写 自定义注解 + 数据源路由

   1、自定义一个注解 DynamicDataSource

package com.huan.springboot.datasource;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* 动态数据源注解
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午8:55:23
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSource {
/**
* 默认是mysql数据源
*
* @return
*/
DynamicDataSourceTypeEnum value() default DynamicDataSourceTypeEnum.MYSQL;
}

   2、编写一个数据源枚举类,用来表示有哪些数据源,防止手动书写会写错

package com.huan.springboot.datasource;

import lombok.AllArgsConstructor;
import lombok.Getter; /**
* 动态数据源类型枚举
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午8:55:52
*/
@AllArgsConstructor
@Getter
public enum DynamicDataSourceTypeEnum { ORACLE("oracle", "oracle数据源"), //
MYSQL("mysql", "当前使用的是mysql数据源"); private String type;
private String desc;
}

 3、程序在需要切换数据源的方法会将上面的这个枚举类型保存,那么保存到哪里呢,肯定是线程安全的ThreadLocal中,因此需要这个类DynamicDataSourceTypeHolder

package com.huan.springboot.datasource;

/**
* 保存当前线程的数据源类型
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午8:59:09
*/
public class DynamicDataSourceTypeHolder { private static final ThreadLocal<DynamicDataSourceTypeEnum> DATA_SOURCE_TYPE = new ThreadLocal<>(); public static void setDataSourceType(DynamicDataSourceTypeEnum dataSourceTypeEnum) {
DATA_SOURCE_TYPE.set(dataSourceTypeEnum);
} public static DynamicDataSourceTypeEnum getDataSourceType() {
return DATA_SOURCE_TYPE.get();
} public static void clear() {
DATA_SOURCE_TYPE.set(null);
} }

4、当我们在需要切换数据源的方法上编写了 @DynamicDataSource 注解后,就需要有一个类来处理这个注解,因此就有了下面这个类。

package com.huan.springboot.datasource;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; /**
* 动态数据源切面
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午9:15:49
*/
@Component
@Aspect
@Order(0)
public class DynamicDataSourceAspect {
@Around("@annotation(dataSource)")
public Object invoked(ProceedingJoinPoint pjp, DynamicDataSource dataSource) throws Throwable {
DynamicDataSourceTypeHolder.setDataSourceType(dataSource.value());
try {
return pjp.proceed();
} finally {
DynamicDataSourceTypeHolder.clear();
}
}
}

   注意:

1、看 @Order里面的值,这个值比较小,会保证这个切面在事务切面之前执行。可以删除这个注解然后看一下效果。

2、这个类中将当前数据源的 key 保存到了 ThreadLocal 类型的变量中

5、有了上面这些内容,那么我们肯定需要 需要编写 动态数据源路由,用于决定是返回那个数据源key ,决定使用的是那个数据源

package com.huan.springboot.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* 动态的数据源路由,由此类决定具体使用的是那个数据源
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午9:01:58
*/
public class DynamicDataSourceRouter extends AbstractRoutingDataSource { @Override
protected Object determineCurrentLookupKey() {
DynamicDataSourceTypeEnum dataSourceType = DynamicDataSourceTypeHolder.getDataSourceType();
if (null == dataSourceType) {
System.out.println("没有获取到数据源,使用默认的数据源.");
return null;
} else {
System.out.println(dataSourceType.getDesc());
return dataSourceType.getType();
}
}

注意:

determineCurrentLookupKey() 这个方法返回了一个key ,那么Spring就知道使用那个数据源。那么Spring是怎么知道的呢 ,看下方的 DynamicDataSourceRoteConfig 配置类,这个类我上方还没有写。

6、上方编写了动态数据源路由,但是数据源的key和具体的数据源还没有进行配置,因此由下方这个配置类来进行配置。

package com.huan.springboot.config;

import java.util.HashMap;
import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement; import com.huan.springboot.datasource.DynamicDataSourceRouter;
import com.huan.springboot.datasource.DynamicDataSourceTypeEnum; /**
* 数据源路由配置
*
* @描述
* @作者 huan
* @时间 2018年3月8日 - 下午9:15:02
*/
@Configuration
@EnableTransactionManagement
public class DynamicDataSourceRoteConfig { @Autowired
@Lazy
private DataSource mysqlDataSource;
@Autowired
@Lazy
private DataSource oracleDataSource; @Bean
@Primary
public DataSource dynamicDataSourceRoute() {
DynamicDataSourceRouter dataSourceRouter = new DynamicDataSourceRouter();
// 默认是mysql 数据源
dataSourceRouter.setDefaultTargetDataSource(mysqlDataSource);
// 映射某个key 为具体的数据源
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DynamicDataSourceTypeEnum.MYSQL.getType(), mysqlDataSource);
targetDataSources.put(DynamicDataSourceTypeEnum.ORACLE.getType(), oracleDataSource); dataSourceRouter.setTargetDataSources(targetDataSources);
return dataSourceRouter;
} }

 注意: 在dynamicDataSourceRoute()方法中

1、使用了@Primary注解修饰,那么当需要使用到数据源的时候会注入这个,而不会注入有上方MultiDataSourceConfig配置的2个数据源。

2、在这个方法中设置了一个默认的数据源,即当没有获取到数据源的key时应该使用那个数据源

3、在这个方法中 有一个  dataSourceRouter.setTargetDataSources(targetDataSources); 这句。在这个里面有一个属性 targetDataSources ,可以看到它的key 和具体的数据源进行关联了。

到此动态切换数据源的代码就写完了,那么下方写一个小例子测试一下。

四、示例

1、示例代码
spring、spring boot中配置多数据源
 

2、查看结果
spring、spring boot中配置多数据源