开发中需要碰到的场景,需要将现有的一套系统提供给另外一个项目组进行使用,但是服务器还是用我们现有的,只是多配置两个数据库进行数据隔离,因此需要用到多数据源进行数据切换。
配置多数据源两种方法,我目前的场景需要用到第一种进行配置
实现多数据源的配置其实并不复杂,很多博客也有具体的实现步骤,为了透彻的理解整个实现原理,这里介绍下spring底层是如何操作数据库的。
当我们发起一个请求涉及到数据库操作时,spring会调用ORM持久层框架(例:mybatis)对应的api;我们都知道任何的持久层框架要连接数据库,都离不开JDBC。因此我们需要用到spring-jdbc模块,spring-jdbc模块提供了一个connection给ORM框架,在调用getConnection方法时注入数据源,数据源内则配置了jdbc连接需要的参数,此时,ORM就可以连接并操作数据库了。
上一步讲述了spring连接数据库的原理步骤,通过上述步骤可以看出,我们需要连接不同的数据库时,最好的改动点就是在注入数据源的时候进行修改,因为数据源是我们自己配置的,具体要怎么操作呢?
首先,我们需要自定义一个动态的数据源DynamicDataSource实现DataSource,然后结合自己的实际业务需求,定义一个标识来区分需要获取哪个数据源,比如我的系统每个用户都有对应的角色和项目组,那么我就可以根据他对应的角色或分组进行判断,来注入不同的dataSource。
初始版本demo代码示例
前提:建议新建一个demo项目,写一个简单的查库接口,然后跟着下面代码进行操作(如果本地或服务器装有mysql,建议新建两个database,在database下创建一张同名的表,方便同一套代码切换数据源查询不同库,可以更直观的看到效果。
我这边创建了名为wp和wpa的两个database,在databse下建立了一张user表)
server:
port: 8099
spring:
datasource:
datasource1:
username: root
password: 123456
url: jdbc:mysql://22.12.193.64:3306/datasource1?serverTimezone=GMT&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowMultiQueries=true
driver-class-name: com.mysql.jdbc.Driver
datasource2:
username: root
password: 123456
url: jdbc:mysql://22.12.193.64:3306/datasource2?serverTimezone=GMT&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowMultiQueries=true
driver-class-name: com.mysql.jdbc.Driver
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.wp.demo.entity
@Configuration
public class DatasourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 通过配置地址拿到spring.datasource中的配置,创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 通过配置地址拿到spring.datasource中的配置,创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
}
@Component
@Primary // 有多个datasource时,将此打他source设置为主要注入的bean,primary
public class DynamicDatasource implements DataSource, InitializingBean {
// 数据源标识,为了保证线程安全使用threadLocal
public static ThreadLocal<String> name = new ThreadLocal<>();
@Autowired
DataSource dataSource1;
@Autowired
DataSource dataSource2;
@Override
public Connection getConnection() throws SQLException {
// 通过不同的标识动态分配数据源
if ("R".equals(name.get())) {
return dataSource1.getConnection();
}
if ("W".equals(name.get())) {
return dataSource2.getConnection();
}
return dataSource1.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@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;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
/**
* 实现了InitializingBean的默认方法
* spring Bean初始化回调
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 初始化
name.set("W");
}
}
到这里,基本就配置好了,我们只需要在controller内请求之前,加上判断数据源的标识,就可以动态切换数据源,来请求wp和wpa两个不同库的数据了,controller代码示例如下:
@GetMapping("/user")
public Result getUser(@RequestParam("id") int id){
// 设置数据源标识
DynamicDatasource.name.set("R");
log.info(String.format("查询datasource1数据源的%s号用户", id));
return userService.selectById(id);
}
@GetMapping("/user2")
public Result getUser2(@RequestParam("id") int id){
DynamicDatasource.name.set("W");
log.info(String.format("查询datasource2数据源的%s号用户", id));
return userService.selectById(id);
}
当然,这只是个demo帮助我们理解流程的,这套代码并不完美,缺点如下。
优化版本代码示例
其实上述的一些问题,spring已经为我们提供了解决方案,我们可以用到AbstractRoutingDataSource抽象类,这个类并不复杂,下面简单介绍下这个类。
// 重点看这个类的几个成员变量
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 需要自己去指定的,所有数据源(targetDataSources)
@Nullable
private Map<Object, Object> targetDataSources;
// 需要指定一个默认数据源(defaultTargetDataSource)
@Nullable
private Object defaultTargetDataSource;
// 会自动将targetDataSources赋值给自己(resolvedDataSources)
@Nullable
private Map<Object, DataSource> resolvedDataSources;// 是负责最终切换数据源的map
}
了解了这个类需要赋值的一些属性,我们就可以开始使用了。
@Component
@Primary // 有多个datasource时,将此打他source设置为主要注入的bean,primary
public class DynamicDatasource extends AbstractRoutingDataSource {
// 数据源标识,为了保证线程安全使用threadLocal
public static ThreadLocal<String> name = new ThreadLocal<>();
@Autowired
DataSource dataSource1;
@Autowired
DataSource dataSource2;
/**
* 此方法作用是返回当前数据源标识
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
/**
* 重写父类方法之前,为父类基础属性进行赋值
*/
@Override
public void afterPropertiesSet() {
// 为AbstractRoutingDataSource的主要参数进行赋值
// targetDataSources初始化所有数据源
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("W", dataSource1);
targetDataSources.put("R", dataSource2);
super.setTargetDataSources(targetDataSources);
// defaultTargetDataSource设置默认数据源
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
新建DynamicDatasourceAspect切面类,拦截所有请求,根据具体业务解析请求头,来实现分配不同的数据源
@Component
@Aspect
@Slf4j
public class DynamicDatasourceAspect {
@Before(value = "execution(public * com.example.demo.controller.*.*(..))")
public void before(JoinPoint jp) {
String name = "解析token获取用户对应的数据源";
// 通过反射拿到请求的相关信息,进行解析,获取相应的标识
// 根据具体业务对应的标识设置请求哪个数据源
DynamicDatasource.name.set("W");
log.info(name);
}
}