一般情况下,不会有多数据源这样的场景出现,但老项目或者特殊需求的项目,可能会有这样的场景
- 同一个应用需要访问两个数据库
- 不用数据库中间件的读写分离
声明两个数据源实例,在getConnection的时候根据业务的不同,注入不同数据源的连接
准备sql脚本,建立两个库,这里mysql为例
create database stu;
create database tech;
use stu;
create table student
(
id varchar(50) not null comment '主键',
name varchar(50) null comment '姓名',
stu_no varchar(50) null comment '学号',
constraint student_pk primary key (id)
);
insert into student values ('1','张同学','111');
insert into student values ('2','李同学','222');
use tech;
create table teacher
(
id varchar(50) not null comment '主键',
name varchar(50) null comment '姓名',
teach_no varchar(50) null comment '教师号',
constraint teacher_pk primary key (id)
);
insert into teacher values ('1','王老师','111');
insert into teacher values ('2','高老师','222');
server:
port: 9000
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://shilei.tech:3306/stu?useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: root123456
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://shilei.tech:3306/tech?useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: root123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
min-idle: 1
max-active: 20
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.datasource.dynamicdatasource.model
package com.datasource.dynamicdatasource.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* @author sl
*/
@Configuration
public class DataSourceConfig {
@Bean("dataSource1")
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Bean("dataSource2")
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
}
package com.datasource.dynamicdatasource.config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* @author sl
* @Primary主要注入的bean
*/
@Configuration
@Primary
public class DynamicDataSource implements DataSource {
public static ThreadLocal nameFlag = new ThreadLocal<>();
@Resource
private DataSource dataSource1;
@Resource
private DataSource dataSource2;
@Override
public Connection getConnection() throws SQLException {
if("student".equals(nameFlag.get())){
return dataSource1.getConnection();
}
return dataSource2.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public T unwrap(Class iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
package com.datasource.dynamicdatasource.controller;
import com.datasource.dynamicdatasource.config.DynamicDataSource;
import com.datasource.dynamicdatasource.model.Student;
import com.datasource.dynamicdatasource.model.Teacher;
import com.datasource.dynamicdatasource.service.StudentService;
import com.datasource.dynamicdatasource.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author sl
*/
@RestController
public class TestDataSourceController {
@Autowired
private StudentService studentService;
@Autowired
private TeacherService teacherService;
@GetMapping("/stu")
public String getStu(){
DynamicDataSource.nameFlag.set("student");
List allStudent = studentService.findAllStudent();
return allStudent.toString();
}
@GetMapping("/tech")
public String getTech(){
DynamicDataSource.nameFlag.set("teacher");
List allTeacher = teacherService.findAllTeacher();
return allTeacher.toString();
}
}
效果如下所示
实现DataSource接口我们本质上只使用了一个方法,就是getConnection()这个无参的方法,其他方法,当内部调用时可能会导致错误,我们不可能实现所有的方法,所以我们继承AbstractRoutingDataSource抽象类
可以看到AbstractRoutingDataSource继承自DataSource,提供了一些实现方法
targetDataSources 所有数据源 (需指定)
defaultTargetDataSource 默认数据源(需指定)
resolvedDataSources= targetDataSources 负责最终切换的数据源map 等于 tagetDataSources
package com.datasource.dynamicdatasource.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author sl
* @Primary主要注入的bean
*/
@Configuration
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
public static ThreadLocal nameFlag = new ThreadLocal<>();
@Resource
private DataSource dataSource1;
@Resource
private DataSource dataSource2;
@Override
protected Object determineCurrentLookupKey() {
// 返回当前数据源的标识
return nameFlag.get();
}
@Override
public void afterPropertiesSet() {
// 为targetDataSources 初始化所有数据源
Map
看一段源码,就是通过determineCurrentLookupKey获取数据源的key,然后去resolvedDataSources中取数据源,resolvedDataSources数据源其实就是targetDataSources
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
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 + "]");
}
return dataSource;
}
package com.datasource.dynamicdatasource.controller;
import com.datasource.dynamicdatasource.config.DynamicDataSource;
import com.datasource.dynamicdatasource.model.Student;
import com.datasource.dynamicdatasource.model.Teacher;
import com.datasource.dynamicdatasource.service.StudentService;
import com.datasource.dynamicdatasource.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author sl
*/
@RestController
public class TestDataSourceController {
@Autowired
private StudentService studentService;
@Autowired
private TeacherService teacherService;
@GetMapping("/stu")
public String getStu(){
// 默认数据源就是student
List allStudent = studentService.findAllStudent();
return allStudent.toString();
}
@GetMapping("/tech")
public String getTech(){
DynamicDataSource.nameFlag.set("teacher");
List allTeacher = teacherService.findAllTeacher();
return allTeacher.toString();
}
}
数据源的切换还是使用AbstractRoutingDataSource,只不过切换方式采用aop拦截自定义注解切换数据源,这种方式也是mybatis-plus多数据源插件所采用的方式
package com.datasource.dynamicdatasource.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author sl
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyDataSource {
String value() default "student";
}
org.springframework.boot
spring-boot-starter-aop
package com.datasource.dynamicdatasource.aspect;
import com.datasource.dynamicdatasource.annotation.MyDataSource;
import com.datasource.dynamicdatasource.config.DynamicDataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author sl
*@Aspect 标识是一个切面
*/
@Aspect
@Component
public class DatasourceAspect {
/**
* 切点规则
*/
@Pointcut("@annotation(com.datasource.dynamicdatasource.annotation.MyDataSource)")
public void pointcut() {
}
@Before("pointcut()")
public void dataSourceAspect(JoinPoint joinPoint){
// 获取方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 判断方法中是否添加了注解
if(method.isAnnotationPresent(MyDataSource.class)){
// 获取方法上的注解
MyDataSource annotation = method.getAnnotation(MyDataSource.class);
String value = annotation.value();
// 设置数据源
DynamicDataSource.nameFlag.set(value);
}
}
}
@GetMapping("/tech")
@MyDataSource("teacher")
public String getTech(){
List allTeacher = teacherService.findAllTeacher();
return allTeacher.toString();
}
官方文档及搭建指南地址:多数据源 | MyBatis-Plus
com.baomidou
dynamic-datasource-spring-boot-starter
3.6.1
server:
port: 9000
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
dynamic:
#设置默认的数据源或者数据源组,默认值即为master
primary: master
#严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master:
url: jdbc:mysql://shilei.tech:3306/stu?useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: root123456
driver-class-name: com.mysql.cj.jdbc.Driver
teacher:
url: jdbc:mysql://shilei.tech:3306/tech?useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: root123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
min-idle: 1
max-active: 20
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.datasource.dynamicdatasource.model
数据源切换使用@DS注解,不使用此注解,使用默认数据源,方法上使用>类上使用
package com.datasource.dynamicdatasource.controller;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.datasource.dynamicdatasource.model.Student;
import com.datasource.dynamicdatasource.model.Teacher;
import com.datasource.dynamicdatasource.service.StudentService;
import com.datasource.dynamicdatasource.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author sl
*/
@RestController
public class TestDataSourceController {
@Autowired
private StudentService studentService;
@Autowired
private TeacherService teacherService;
@GetMapping("/stu")
public String getStu(){
List allStudent = studentService.findAllStudent();
return allStudent.toString();
}
@GetMapping("/tech")
@DS("teacher")
public String getTech(){
List allTeacher = teacherService.findAllTeacher();
return allTeacher.toString();
}
}
项目启动日志中可以看到两个数据源的加载信息
访问tech以及stu都能正常访问,代表动态数据源添加成功
使用多数据源要注意事务的控制,提交和回滚策略,可以观看spring多数据源事务解决方案