SpringBoot系列 - 多数据源配置
项目中经常会出现需要同时连接两个数据源的情况,这里还是演示基于MyBatis来配置两个数据源,并演示如何切换不同的数据源。
网上的一些例子都写的有点冗余,这里我通过自定义注解+AOP的方式,来简化这种数据源的切换操作。
maven依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <druid.version>1.1.2</druid.version> <mysql-connector.version>8.0.7-dmr</mysql-connector.version> <mybatis-plus.version>2.1.8</mybatis-plus.version> <mybatisplus-spring-boot-starter.version>1.0.5</mybatisplus-spring-boot-starter.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatisplus-spring-boot-starter</artifactId> <version>${mybatisplus-spring-boot-starter.version}</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> <version>1.3</version> <scope>test</scope> </dependency> </dependencies>
|
初始化数据库
这里我们需要创建两个数据库,初始化脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| CREATE DATABASE IF NOT EXISTS pos default charset utf8 COLLATE utf8_general_ci; SET FOREIGN_KEY_CHECKS=0; USE pos;
DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(32) NOT NULL COMMENT '账号', `name` VARCHAR(16) DEFAULT '' COMMENT '名字', `password` VARCHAR(128) DEFAULT '' COMMENT '密码', `salt` VARCHAR(64) DEFAULT '' COMMENT 'md5密码盐', `phone` VARCHAR(32) DEFAULT '' COMMENT '联系电话', `tips` VARCHAR(255) COMMENT '备注', `state` TINYINT(1) DEFAULT 1 COMMENT '状态 1:正常 2:禁用', `created_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='后台管理用户表';
INSERT INTO `t_user` VALUES (1,'admin','系统管理员','123456','www', '17890908889', '系统管理员', 1, '2017-12-12 09:46:12', '2017-12-12 09:46:12'); INSERT INTO `t_user` VALUES (2,'aix','张三','123456','eee', '17859569358', '', 1, '2017-12-12 09:46:12', '2017-12-12 09:46:12');
CREATE DATABASE IF NOT EXISTS pos default charset utf8 COLLATE utf8_general_ci; SET FOREIGN_KEY_CHECKS=0; USE biz;
DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(32) NOT NULL COMMENT '账号', `name` VARCHAR(16) DEFAULT '' COMMENT '名字', `password` VARCHAR(128) DEFAULT '' COMMENT '密码', `salt` VARCHAR(64) DEFAULT '' COMMENT 'md5密码盐', `phone` VARCHAR(32) DEFAULT '' COMMENT '联系电话', `tips` VARCHAR(255) COMMENT '备注', `state` TINYINT(1) DEFAULT 1 COMMENT '状态 1:正常 2:禁用', `created_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='后台管理用户表';
INSERT INTO `t_user` VALUES (1,'admin1','系统管理员','123456','www', '17890908889', '系统管理员', 1, '2017-12-12 09:46:12', '2017-12-12 09:46:12'); INSERT INTO `t_user` VALUES (2,'aix1','张三','123456','eee', '17859569358', '', 1, '2017-12-12 09:46:12', '2017-12-12 09:46:12');
|
可以看到我创建了两个数据库pos和biz,同时还初始化了用户表,并分别插入两条初始数据。注意用户名数据不相同。
配置文件
接下来修改application.yml配置文件,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| xncoding: muti-datasource-open: true
mybatis-plus: mapper-locations: classpath*:com/xncoding/pos/common/dao/repository/mapping/*.xml typeAliasesPackage: > com.xncoding.pos.common.dao.entity global-config: id-type: 0 db-column-underline: false refresh-mapper: true configuration: map-underscore-to-camel-case: true cache-enabled: true lazyLoadingEnabled: true multipleResultSetsEnabled: true
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/pos?useSSL=false&autoReconnect=true&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf8 username: root password: 123456
biz: datasource: url: jdbc:mysql://127.0.0.1:3306/biz?useSSL=false&autoReconnect=true&tinyInt1isBit=false&useUnicode=true&characterEncoding=utf8 username: root password: 123456
|
解释一下:
这里我添加了一个自定义配置项muti-datasource-open
,用来控制是否开启多数据源支持。这个配置项后面我会用到。 接下来定义MyBatis的配置,最后定义了两个MySQL数据库的连接信息,一个是pos库,一个是biz库。
动态切换数据源
这里通过Spring的AOP技术实现数据源的动态切换。
多数据源的常量类:
1 2 3 4
| public interface DSEnum { String DATA_SOURCE_CORE = "dataSourceCore"; String DATA_SOURCE_BIZ = "dataSourceBiz"; }
|
datasource的上下文,用来存储当前线程的数据源类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setDataSourceType(String dataSourceType) { contextHolder.set(dataSourceType); }
public static String getDataSourceType() { return contextHolder.get(); }
public static void clearDataSourceType() { contextHolder.remove(); } }
|
定义动态数据源,继承AbstractRoutingDataSource
:
1 2 3 4 5 6 7
| public class DynamicDataSource extends AbstractRoutingDataSource {
@Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceType(); } }
|
接下来自定义一个注解,用来在Service方法上面注解使用哪个数据源:
1 2 3 4 5 6 7 8 9 10 11
|
@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface DataSource { String name() default ""; }
|
最后,最核心的AOP类定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
@Aspect @Component @ConditionalOnProperty(prefix = "xncoding", name = "muti-datasource-open", havingValue = "true") public class MultiSourceExAop implements Ordered {
private Logger log = Logger.getLogger(this.getClass());
@Pointcut(value = "@annotation(com.xncoding.pos.common.annotion.DataSource)") private void cut() {
}
@Around("cut()") public Object around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature(); MethodSignature methodSignature = null; if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("该注解只能用于方法"); } methodSignature = (MethodSignature) signature;
Object target = point.getTarget(); Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
DataSource datasource = currentMethod.getAnnotation(DataSource.class); if (datasource != null) { DataSourceContextHolder.setDataSourceType(datasource.name()); log.debug("设置数据源为:" + datasource.name()); } else { DataSourceContextHolder.setDataSourceType(DSEnum.DATA_SOURCE_CORE); log.debug("设置数据源为:dataSourceCore"); } try { return point.proceed(); } finally { log.debug("清空数据源信息!"); DataSourceContextHolder.clearDataSourceType(); } }
@Override public int getOrder() { return 1; }
}
|
这里使用到了注解@ConditionalOnProperty,只有当我的配置文件中muti-datasource-open=true的时候注解才会生效。
另外还有一个要注意的地方,就是order,aop的顺序一定要早于spring的事务,这里我将它设置成1,后面你会看到我将spring事务顺序设置成2。
配置类
首先有两个属性类:
DruidProperties
连接池的属性类
MutiDataSourceProperties
biz数据源的属性类
然后定义配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @Configuration @EnableTransactionManagement(order = 2) @MapperScan(basePackages = {"com.xncoding.pos.common.dao.repository"}) public class MybatisPlusConfig {
@Autowired DruidProperties druidProperties;
@Autowired MutiDataSourceProperties mutiDataSourceProperties;
private DruidDataSource coreDataSource() { DruidDataSource dataSource = new DruidDataSource(); druidProperties.config(dataSource); return dataSource; }
private DruidDataSource bizDataSource() { DruidDataSource dataSource = new DruidDataSource(); druidProperties.config(dataSource); mutiDataSourceProperties.config(dataSource); return dataSource; }
@Bean @ConditionalOnProperty(prefix = "xncoding", name = "muti-datasource-open", havingValue = "false") public DruidDataSource singleDatasource() { return coreDataSource(); }
@Bean @ConditionalOnProperty(prefix = "xncoding", name = "muti-datasource-open", havingValue = "true") public DynamicDataSource mutiDataSource() {
DruidDataSource coreDataSource = coreDataSource(); DruidDataSource bizDataSource = bizDataSource();
try { coreDataSource.init(); bizDataSource.init(); } catch (SQLException sql) { sql.printStackTrace(); }
DynamicDataSource dynamicDataSource = new DynamicDataSource(); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(DSEnum.DATA_SOURCE_CORE, coreDataSource); hashMap.put(DSEnum.DATA_SOURCE_BIZ, bizDataSource); dynamicDataSource.setTargetDataSources(hashMap); dynamicDataSource.setDefaultTargetDataSource(coreDataSource); return dynamicDataSource; } }
|
代码其实很好理解,我就不再多做解释了。
然后步骤跟普通的集成MyBatis是一样的,我简单的过一遍。
实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @TableName(value = "t_user") public class User extends Model<User> {
private static final long serialVersionUID = 1L;
@TableId(value="id", type= IdType.AUTO) private Integer id;
private String username;
private String name;
private String password;
private String salt;
private String phone;
private String tips;
private Integer state;
private Date createdTime;
private Date updatedTime; }
|
定义DAO
1 2 3
| public interface UserMapper extends BaseMapper<User> {
}
|
定义Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Service public class UserService {
@Resource private UserMapper userMapper;
public User findById(Integer id) { return userMapper.selectById(id); }
@DataSource(name = DSEnum.DATA_SOURCE_BIZ) public User findById1(Integer id) { return userMapper.selectById(id); }
public void insertUser(User user) { userMapper.insert(user); }
public void updateUser(User user) { userMapper.updateById(user); }
public void deleteUser(Integer id) { userMapper.deleteById(id); }
}
|
这里唯一要说明的就是我在方法findById1()
上面增加了注解@DataSource(name = DSEnum.DATA_SOURCE_BIZ)
,这样这个方法就会访问biz数据库。
注意,不加注解就会访问默认数据库pos。
测试
最后编写一个简单的测试,我只测试findById()
方法和findById1()
方法,看它们是否访问的是不同的数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { private static final Logger log = LoggerFactory.getLogger(ApplicationTests.class);
@Resource private UserService userService;
@Test public void test() { User user = userService.findById(1); assertThat(user.getUsername(), is("admin"));
User user1 = userService.findById1(1); assertThat(user1.getUsername(), is("admin1")); } }
|
运行测试,结果显示为green bar,成功!
GitHub源码
springboot-multisource —已测试
来源
https://www.xncoding.com/