springboot + mybatis-plus 基于注解的多数据源
2020年6月21日大约 6 分钟
业务背景
上一节我们讲解通过分包来实现多数据源,这一节来讲解另一种实现方式——通过注解,动态切换数据源。
准备工作
建表语句
- master
模拟主库:
CREATE DATABASE IF NOT EXISTS test DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ;
create table test.master (
id int(20) NOT NULL primary key AUTO_INCREMENT comment '主键',
name varchar(32) NOT NULL comment '名称',
password varchar(32) NOT NULL comment '密码'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 comment '主';
- slave
模拟从库:
CREATE DATABASE IF NOT EXISTS test_slave DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ;
create table test_slave.slave (
id int(20) NOT NULL primary key AUTO_INCREMENT comment '主键',
name varchar(32) NOT NULL comment '名称',
password varchar(32) NOT NULL comment '密码'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 comment '从者';
MPG
mybatis-plus 自动生产实现,从库调整下对应的连接和包即可。
import com.baomidou.mybatisplus.enums.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DbType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MasterMPGTest {
/**
* 覆盖生成下列文件,第一次建表时使用
* 1. java mapper
* 2. java enity
* 3. xml mapper
* 4. service
*
* 更新时使用:
* 1. 只修改对应的实体类即可,其他科注释掉。
*
* genDalJavaEntity
*/
public static void main(String[] args) {
String[] tables = new String[]{
"master",
};
genDalJavaEntity(tables);
genDalJavaMapper(tables);
genDalXml(tables);
genService(tables);
}
private static final String BASE_DIR = System.getProperty("user.dir");
private static AutoGenerator initConfig(String... tables) {
final String author = System.getProperty("user.name");
//创建代码生成器
AutoGenerator mpg = new AutoGenerator();
//指定模板引擎 默认velocity
//mpg.setTemplateEngine(new FreemarkerTemplateEngine());
//全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOpen(false);
gc.setOutputDir(BASE_DIR);
gc.setFileOverride(true); //是否覆盖已有文件
gc.setBaseResultMap(false); //XML是否需要BaseResultMap
gc.setBaseColumnList(false); //XML是否显示字段
gc.setControllerName("%sController");
gc.setServiceName("%sService");
gc.setServiceImplName("%sServiceImpl");
gc.setMapperName("%sMapper");
gc.setXmlName("%sMapper");
gc.setAuthor(author);
gc.setEnableCache(false);
gc.setIdType(IdType.AUTO);
mpg.setGlobalConfig(gc);
//数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDbType(DbType.MYSQL);
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUrl("jdbc:mysql://localhost:3306/test");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
//策略配置
StrategyConfig sc = new StrategyConfig();
sc.setNaming(NamingStrategy.underline_to_camel); //表名生成策略
sc.setEntityBuilderModel(false);
sc.setCapitalMode(true);
sc.setEntityLombokModel(false);
sc.setDbColumnUnderline(true);
sc.setEntityColumnConstant(false); //生成字段常量
// 指定表信息
sc.setInclude(tables);
sc.entityTableFieldAnnotationEnable(true);
mpg.setStrategy(sc);
//包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.github.houbb.mp.sb.learn.mysql");
pc.setEntity("dal.entity.master");
pc.setMapper("dal.mapper.master");
pc.setXml("dal.mapper.master");
pc.setService("service.service.master");
pc.setServiceImpl("service.service.master.impl");
mpg.setPackageInfo(pc);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
//控制 不生成 controller 空字符串就行
templateConfig.setController("");
templateConfig.setService("");
templateConfig.setServiceImpl("");
templateConfig.setEntity("");
templateConfig.setMapper("");
templateConfig.setXml("");
mpg.setTemplate(templateConfig);
return mpg;
}
/**
* 生成 dal 的 java 代码
* @param tables 表名称
* @since 1.0.0
*/
private static void genDalJavaEntity(String... tables) {
String moduleName = "mp-sb-mysql";
AutoGenerator mpg = initConfig(tables);
mpg.getGlobalConfig().setOutputDir(BASE_DIR+"/"+moduleName+"\\src\\main\\java\\");
mpg.getTemplate().setEntity(ConstVal.TEMPLATE_ENTITY_JAVA);
mpg.execute();
}
/**
* 生成 dal 的 java mapper 代码
* @param tables 表名称
* @since 1.0.0
*/
private static void genDalJavaMapper(String... tables) {
String moduleName = "mp-sb-mysql";
AutoGenerator mpg = initConfig(tables);
mpg.getGlobalConfig().setOutputDir(BASE_DIR+"/"+moduleName+"\\src\\main\\java\\");
mpg.getTemplate().setMapper(ConstVal.TEMPLATE_MAPPER);
mpg.execute();
}
/**
* @param tables 表名称
* @since 1.0.0
*/
private static void genDalXml(String... tables) {
String moduleName = "mp-sb-mysql";
AutoGenerator mpg = initConfig(tables);
mpg.getGlobalConfig().setOutputDir(BASE_DIR+"/"+moduleName+"\\src\\main\\resources\\");
mpg.getTemplate().setXml(ConstVal.TEMPLATE_XML);
mpg.execute();
}
private static void genService(String... tables) {
String moduleName = "mp-sb-mysql";
AutoGenerator mpg = initConfig(tables);
mpg.getGlobalConfig().setOutputDir(BASE_DIR+"/"+moduleName+"\\src\\main\\java\\");
mpg.getTemplate().setService(ConstVal.TEMPLATE_SERVICE);
mpg.getTemplate().setServiceImpl(ConstVal.TEMPLATE_SERVICEIMPL);
mpg.execute();
}
}
maven 依赖
4.0.0
mp-sb-mysql
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
3.4.4
1.3.1
2.3
1.8
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis
mybatis
${mybatis.version}
org.mybatis
mybatis-spring
${mybatis-spring.version}
com.baomidou
mybatis-plus
${mybatis-plus.version}
mybatis
org.mybatis
mybatis-spring
org.mybatis
mysql
mysql-connector-java
5.1.47
runtime
com.alibaba
druid-spring-boot-starter
1.1.14
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.apache.velocity
velocity-engine-core
2.0
org.springframework.boot
spring-boot-configuration-processor
true
org.aspectj
aspectjrt
1.8.10
org.aspectj
aspectjweaver
1.8.10
整体目录
├─main
│ ├─java
│ │ └─com
│ │ └─github
│ │ └─houbb
│ │ └─mp
│ │ └─sb
│ │ └─learn
│ │ └─mysql
│ │ │ Application.java
│ │ │
│ │ ├─config
│ │ │ DataSourceConfig.java
│ │ │
│ │ ├─dal
│ │ │ ├─entity
│ │ │ │ ├─master
│ │ │ │ │ Master.java
│ │ │ │ │
│ │ │ │ └─slave
│ │ │ │ Slave.java
│ │ │ │
│ │ │ └─mapper
│ │ │ ├─master
│ │ │ │ MasterMapper.java
│ │ │ │
│ │ │ └─slave
│ │ │ SlaveMapper.java
│ │ │
│ │ ├─dynamic
│ │ │ DataBaseContextHolder.java
│ │ │ DataSourceAspect.java
│ │ │ DataSourceType.java
│ │ │ DynamicDataSource.java
│ │ │ DynamicRoute.java
│ │ │
│ │ └─service
│ │ └─service
│ │ ├─master
│ │ │ │ MasterService.java
│ │ │ │
│ │ │ └─impl
│ │ │ MasterServiceImpl.java
│ │ │
│ │ └─slave
│ │ │ SlaveService.java
│ │ │
│ │ └─impl
│ │ SlaveServiceImpl.java
│ │
│ └─resources
│ │ application.yml
│ │
│ └─com
│ └─github
│ └─houbb
│ └─mp
│ └─sb
│ └─learn
│ └─mysql
│ └─dal
│ └─mapper
│ ├─master
│ │ MasterMapper.xml
│ │
│ └─slave
│ SlaveMapper.xml
数据库配置
DataSourceConfig.java
这里我们不再采用分包的方式,直接扫描在一起。
package com.github.houbb.mp.sb.learn.mysql.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import com.github.houbb.mp.sb.learn.mysql.dynamic.DataSourceType;
import com.github.houbb.mp.sb.learn.mysql.dynamic.DynamicDataSource;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import java.util.HashMap;
import java.util.Map;
/**
* @author binbin.hou
* @since 1.0.0
*/
@Configuration
@MapperScan("com.github.houbb.mp.sb.learn.mysql.dal.mapper")
public class DataSourceConfig {
@Bean(name = "datasourceMaster")
@Primary
public DruidDataSource datasourceMaster() {
DruidDataSource source = new DruidDataSource();
source.setUrl("jdbc:mysql://localhost:3306/test");
source.setUsername("root");
source.setPassword("123456");
return source;
}
@Bean(name = "datasourceSlave")
public DruidDataSource datasourceSlave() {
DruidDataSource source = new DruidDataSource();
source.setUrl("jdbc:mysql://localhost:3306/test_slave");
source.setUsername("root");
source.setPassword("123456");
return source;
}
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier("datasourceMaster") DruidDataSource ds1, @Qualifier("datasourceSlave") DruidDataSource ds2) {
Map targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceType.MASTER, ds1);
targetDataSource.put(DataSourceType.SLAVE, ds2);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(ds1);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
// 指定数据源
bean.setDataSource(dynamicDataSource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 直接扫描所有
bean.setMapperLocations(resolver.getResources("classpath*:com/github/houbb/mp/sb/learn/mysql/dal/mapper/**Mapper.xml"));
return bean.getObject();
}
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
动态数据源
- DynamicDataSource.java
用于数据源的切换实现
package com.github.houbb.mp.sb.learn.mysql.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataBaseContextHolder.getDataSourceType();
}
}
- DataBaseContextHolder.java
使用 ThreadLocal 的方式保存数据源信息。
package com.github.houbb.mp.sb.learn.mysql.dynamic;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class DataBaseContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String type) {
if (type == null) {
throw new NullPointerException();
}
contextHolder.set(type);
}
public static String getDataSourceType() {
String type = contextHolder.get();
if (type == null) {
//确定一个默认数据源
return DataSourceType.MASTER;
}
return type;
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
- DataSourceType.java
数据源类型常量
public class DataSourceType {
public static final String MASTER = "master";
public static final String SLAVE = "slave";
}
动态切换数据源
直接使用注解标识,结合 AOP 达到动态切换的效果。
注解定义
package com.github.houbb.mp.sb.learn.mysql.dynamic;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author binbin.hou
* @since 1.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DynamicRoute {
/**
* 主数据源
* @return 数据源名称
* @since 1.0.0
*/
String value() default "";
}
切面实现
package com.github.houbb.mp.sb.learn.mysql.dynamic;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author binbin.hou
* @since 1.0.0
*/
@Aspect
@Component
@EnableAspectJAutoProxy
public class DataSourceAspect {
/**
* 日志实例
* @since 1.0.0
*/
private static final Logger LOG = LoggerFactory.getLogger(DataSourceAspect.class);
/**
* 拦截注解指定的方法
*/
@Pointcut("@annotation(com.github.houbb.mp.sb.learn.mysql.dynamic.DynamicRoute)")
public void pointCut() {
//
}
/**
* 拦截处理
*
* @param point point 信息
* @return result
* @throws Throwable if any
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
// 获取当前拦截的方法签名
String signatureShortStr = point.getSignature().toShortString();
Method method = getCurrentMethod(point);
DynamicRoute route = method.getAnnotation(DynamicRoute.class);
String value = route.value();
// 设置
DataBaseContextHolder.setDataSourceType(value);
return point.proceed();
} finally {
LOG.info("清空类型");
DataBaseContextHolder.clearDataSourceType();
}
}
/**
* 获取当前方法信息
*
* @param point 切点
* @return 方法
*/
private Method getCurrentMethod(ProceedingJoinPoint point) {
try {
Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
效果验证
方法定义
- 主库
@DynamicRoute
标注数据源为 master;
import com.github.houbb.mp.sb.learn.mysql.dal.entity.master.Master;
import com.github.houbb.mp.sb.learn.mysql.dal.mapper.master.MasterMapper;
import com.github.houbb.mp.sb.learn.mysql.dynamic.DynamicRoute;
import com.github.houbb.mp.sb.learn.mysql.service.service.master.MasterService;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class MasterServiceImpl extends ServiceImpl implements MasterService {
@Override
@DynamicRoute("master")
public void create() {
Master master = new Master();
master.setName("master");
master.setPassword("123465");
this.insert(master);
}
}
- 从库
@DynamicRoute
标注数据源为 slave;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.github.houbb.mp.sb.learn.mysql.dal.entity.slave.Slave;
import com.github.houbb.mp.sb.learn.mysql.dal.mapper.slave.SlaveMapper;
import com.github.houbb.mp.sb.learn.mysql.dynamic.DynamicRoute;
import com.github.houbb.mp.sb.learn.mysql.service.service.slave.SlaveService;
import org.springframework.stereotype.Service;
@Service
public class SlaveServiceImpl extends ServiceImpl implements SlaveService {
@Override
@DynamicRoute("slave")
public void create() {
Slave slave = new Slave();
slave.setName("slave");
slave.setPassword("123465");
this.insert(slave);
}
}
主库测试
import com.github.houbb.mp.sb.learn.mysql.Application;
import com.github.houbb.mp.sb.learn.mysql.service.service.master.MasterService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author binbin.hou
* @since 1.0.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= Application.class)
public class MasterServiceTest {
@Autowired
private MasterService masterService;
@Test
public void insertTest() {
masterService.create();
}
}
- 效果
mysql> select * from master;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | master | 123465 |
+----+--------+----------+
1 row in set (0.00 sec)
从库测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= Application.class)
public class SlaveServiceTest {
@Autowired
private SlaveService slaveService;
@Test
public void insertTest() {
slaveService.create();
}
}
- 效果
mysql> select * from slave;
+----+-------+----------+
| id | name | password |
+----+-------+----------+
| 1 | slave | 123465 |
+----+-------+----------+
1 row in set (0.00 sec)
小结
到这里,基于注解的动态数据源切换就实现了。
原理还是比较简单的,感觉这里可以封装为一个公共的方法,而不是每一次都花费时间去自己实现。
参考资料
贡献者
binbin.hou