springboot系列之动态数据源
在经过多数据源的配置后,本章介绍动态数据源的配置,这里采用注解和AOP的方法实现多数据源自动切换。在使用过程中,只需要添加注解就可以使用,简单方便。
application.properties
#mapper.xml文件
mybatis.mapper-locations=classpath:mapper/*.xml
#实体类包
mybatis.type-aliases-package=com.husy.springboot.dynamicsource.entity
#数据源配置(默认)
spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 其他数据源
custom.datasource.names=ds1,ds2
# ds1
custom.datasource.ds1.jdbc-url=jdbc:mysql://localhost:3306/test-db1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
custom.datasource.ds1.username=root
custom.datasource.ds1.password=123456
custom.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
# ds2
custom.datasource.ds2.jdbc-url=jdbc:mysql://localhost:3306/test-db2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
custom.datasource.ds2.username=root
custom.datasource.ds2.password=123456
custom.datasource.ds2.driver-class-name=com.mysql.cj.jdbc.Driver
怎么实现切换?,不要急
Spring提供了AbstractRoutingDataSource来实现这样的功能,AbstractRoutingDataSource可以根据key值动态切换到具体的数据源。有兴趣可以查看一下AbstractRoutingDataSource源码,这里不多描述
配置数据源如下:
/**
* @description: 动态数据源路由配置
* AbstractRoutingDataSource(每执行一次数据库,动态获取DataSource)
* @author: hsy
* @date; 2019/9/24
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
//determineCurrentLookupKey() 用于获取数据源dataSource的key值
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
package com.husy.springboot.dynamicsource.config;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 动态数据源上下文管理
* @author: hsy
* @date; 2019/9/24
*/
public class DynamicDataSourceContextHolder {
//存放当前线程使用的数据源类型信息
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
//存放数据源id
public static List<String> dataSourceIds = new ArrayList<>();
//设置数据源
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
//获取数据源
public static String getDataSourceType() {
return contextHolder.get();
}
//清除数据源
public static void clearDataSourceType() {
contextHolder.remove();
}
//判断当前数据源是否存在
public static boolean containsDataSource(String dataSourceId) {
return dataSourceIds.contains(dataSourceId);
}
}
核心代码
package com.husy.springboot.dynamicsource.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 动态数据源注册
* 实现 ImportBeanDefinitionRegistrar 实现数据源注册
* 实现 EnvironmentAware 用于读取application.yml配置
* 启动动态数据源请在启动类中 添加 @Import(DynamicDataSourceRegister.class)
* @author: hsy
* @date; 2019/9/24
*/
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
// 默认数据源
private DataSource defaultDataSource;
// 存储我们需要注册的数据源
private static Map<String, DataSource> customDataSources = new HashMap<>();
// 配置文件中未指定数据源类型,使用该默认值
// 指定默认数据源(springboot2.0默认数据源是hikari如何想使用其他数据源可以自己配置)
// private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";
private static final String DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource";
private static String DB_PREFIX="custom.datasource";
/**
* 加载多数据源配置
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
// 初始化默认数据源
initDefaultDataSource(environment);
// 初始化自定义的数据源
initCustomDataSources(environment);
}
/**
* 初始化默认数数据源
* @param env
*/
private void initDefaultDataSource(Environment env) {
// 读取主数据源
Map<String, Object> dsMap = new HashMap<>();
dsMap.put("type", env.getProperty ("spring.datasource.type"));
dsMap.put("driver", env.getProperty("spring.datasource.driver-class-name"));
dsMap.put("url", env.getProperty("spring.datasource.jdbc-url"));
dsMap.put("username", env.getProperty("spring.datasource.username"));
dsMap.put("password", env.getProperty("spring.datasource.password"));
defaultDataSource = buildDataSource(dsMap);
}
/**
* 初始化更多数据源
* @param env
*/
private void initCustomDataSources(Environment env) {
// 读取配置文件获取更多数据源,也可以通过defaultDataSource读取数据库获取更多数据源
String dsPrefixs = env.getProperty(DB_PREFIX + ".names" );
for (String dsName : dsPrefixs.split(",")) {// 多个数据源
Map<String, Object> dsMap = new HashMap<>();
dsMap.put("type", env.getProperty(DB_PREFIX + "." + dsName + ".type"));
dsMap.put("driver", env.getProperty(DB_PREFIX + "." + dsName + ".driver-class-name"));
dsMap.put("url", env.getProperty(DB_PREFIX + "." + dsName + ".jdbc-url"));
dsMap.put("username", env.getProperty(DB_PREFIX + "." + dsName + ".username"));
dsMap.put("password", env.getProperty(DB_PREFIX + "." + dsName + ".password"));
DataSource ds = buildDataSource(dsMap);
customDataSources.put(dsName, ds);
}
}
/**
* 将动态数据源注册到spring中
*
* @param importingClassMetadata
* @param registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String, Object> targetDataSources = new HashMap<>();
// 添加默认数据源
targetDataSources.put("dataSource", this.defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
// 添加更多数据源
targetDataSources.putAll(customDataSources);
DynamicDataSourceContextHolder.dataSourceIds.addAll(customDataSources.keySet());
// bean定义类 创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
// 设置bean的类型,此处DynamicDataSource是继承AbstractRoutingDataSource的实现类
beanDefinition.setBeanClass(DynamicDataSource.class);
/*
* 设置属性赋值
* */
// 需要注入的参数
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
// 添加默认数据源,避免key不存在的情况没有数据源可用
mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
// 添加其他数据源
mpv.addPropertyValue("targetDataSources", targetDataSources);
//注册BeanDefinitionRegistry,--> 将该bean注册为datasource,不使用springboot自动生成的datasource
registry.registerBeanDefinition("dataSource", beanDefinition);
log.info("Dynamic DataSource Registry");
}
public DataSource buildDataSource(Map<String, Object> dataSourceMap) {
try {
Object type = dataSourceMap.get("type");
if (type == null) {
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource
}
Class<? extends DataSource> dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
String driverClassName = dataSourceMap.get("driver").toString();
String url = dataSourceMap.get("url").toString();
String username = dataSourceMap.get("username").toString();
String password = dataSourceMap.get("password").toString();
// 自定义DataSource配置
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
AOP切换
/**
* 切换数据注解 可以用于类或者方法级别 方法级别优先级 > 类级别
*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name() ;
}
package com.husy.springboot.dynamicsource.aspect;
import com.husy.springboot.dynamicsource.annotation.TargetDataSource;
import com.husy.springboot.dynamicsource.config.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @description: 数据源切换切面
* @author: hsy
* @date; 2019/9/26
*/
@Slf4j
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {
//改变数据源
@Before("@annotation(targetDataSource)")
public void changeDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
String dbid = targetDataSource.name();
if (!DynamicDataSourceContextHolder.containsDataSource(dbid)) {
//joinPoint.getSignature() :获取连接点的方法签名对象
log.info("数据源 " + dbid + " 不存在,使用默认的数据源 -> " + joinPoint.getSignature());
} else {
log.info("使用数据源:" + dbid);
DynamicDataSourceContextHolder.setDataSourceType(dbid);
}
}
@After("@annotation(targetDataSource)")
public void clearDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
log.info("清除数据源 " + targetDataSource.name() + " !");
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
启动类配置
@SpringBootApplication
@Import({DynamicDataSourceRegister.class}) // 注册动态多数据源
@MapperScan("com.husy.springboot.dynamicsource.mapper")//将项目中对应的mapper类的路径加进来就可以了
public class SpringbootDynamicSourceApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDynamicSourceApplication.class, args);
}
}
使用方式
/**
* <p>
* 服务类
* </p>
*
* @author husy
* @since 2019-09-22
*/
public interface IUsersService{
List<Users> getAll();
Users getOne(Long id);
void insert(Users user);
void update(Users user);
void delete(Long id);
}
/**
* <p>
* 服务实现类
* </p>
*
* @author husy
* @since 2019-09-22
*/
@Service
public class UsersServiceImpl implements IUsersService {
@Resource
UsersMapper usersMapper;
@Override
@TargetDataSource(name="ds1")
public List<Users> getAll() {
return usersMapper.selectList(new QueryWrapper<>());
}
@Override
@TargetDataSource(name="ds2")
public Users getOne(Long id) {
return usersMapper.selectById(id);
}
@Override
@TargetDataSource(name="ds1")
public void insert(Users user) {
usersMapper.insert(user);
}
@Override
@TargetDataSource(name="ds2")
public void update(Users user) {
usersMapper.update(user,null);
}
@Override
@TargetDataSource(name="ds2")
public void delete(Long id) {
usersMapper.deleteById(id);
}
}
/**
* <p>
* 前端控制器
* </p>
*
* @author husy
* @since 2019-09-22
*/
@RestController
@RequestMapping("users")
public class UsersController {
@Autowired
IUsersService usersService;
@RequestMapping("/getUsers")
public List<Users> getUsers() {
List<Users> users=usersService.getAll();
return users;
}
@RequestMapping("/getUser/{id}")
public Users getUser(@PathVariable Long id) {
Users user=usersService.getOne(id);
return user;
}
@RequestMapping("/add")
public void save(@RequestBody Users user) {
usersService.insert(user);
}
@RequestMapping(value="update")
public void update(@RequestBody Users user) {
usersService.update(user);
}
@RequestMapping(value="/delete/{id}")
public void delete(@PathVariable("id") Long id) {
usersService.delete(id);
}
}
测试结果
SQL脚本:
CREATE TABLE `users` (
`user_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_name` VARCHAR(32) NOT NULL COMMENT '用户名' COLLATE 'utf8_general_ci',
`password` VARCHAR(32) NOT NULL COMMENT '密码' COLLATE 'utf8_general_ci',
`user_sex` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '0:男,1:女',
`nick_name` VARCHAR(32) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`user_id`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
这里介绍一个更简单的动态数据源配置
使用mybatis-plus 动态数据源,配置方式及其简单!!!!!!
1、引入dynamic-datasource-spring-boot-starter
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
2、配置数据源
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
slave_1:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
slave_2:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3308/dynamic
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
配置好后直接就可以使用了,省去了自己编写切换的代码
如下:
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List<Map<String, Object>> selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
具体请查看官方文档