业务背景

上一节我们讲解通过分包来实现多数据源,这一节来讲解另一种实现方式——通过注解,动态切换数据源。

准备工作

建表语句

  • master

模拟主库:

  [sql]
1
2
3
4
5
6
7
8
9
10
11
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

模拟从库:

  [sql]
1
2
3
4
5
6
7
8
9
10
11
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 自动生产实现,从库调整下对应的连接和包即可。

  [java]
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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 依赖

  [xml]
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <artifactId>mp-sb-mysql</artifactId> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <mybatis.version>3.4.4</mybatis.version> <mybatis-spring.version>1.3.1</mybatis-spring.version> <mybatis-plus.version>2.3</mybatis-plus.version> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- mybatis-plus--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>${mybatis-plus.version}</version> <exclusions> <exclusion> <artifactId>mybatis</artifactId> <groupId>org.mybatis</groupId> </exclusion> <exclusion> <artifactId>mybatis-spring</artifactId> <groupId>org.mybatis</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <!-- alibaba的druid数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.14</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mybatis-plus自动化工具需要的依赖 模板--> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.10</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.10</version> </dependency> </dependencies> </project>

整体目录

  [plaintext]
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
66
67
├─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

这里我们不再采用分包的方式,直接扫描在一起。

  [java]
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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<Object, Object> 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

用于数据源的切换实现

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的方式保存数据源信息。

  [java]
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
package com.github.houbb.mp.sb.learn.mysql.dynamic; /** * @author binbin.hou * @since 1.0.0 */ public class DataBaseContextHolder { private static final ThreadLocal<String> 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

数据源类型常量

  [java]
1
2
3
4
5
6
7
public class DataSourceType { public static final String MASTER = "master"; public static final String SLAVE = "slave"; }

动态切换数据源

直接使用注解标识,结合 AOP 达到动态切换的效果。

注解定义

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 ""; }

切面实现

  [java]
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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;

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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<MasterMapper, Master> implements MasterService { @Override @DynamicRoute("master") public void create() { Master master = new Master(); master.setName("master"); master.setPassword("123465"); this.insert(master); } }
  • 从库

@DynamicRoute 标注数据源为 slave;

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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<SlaveMapper, Slave> implements SlaveService { @Override @DynamicRoute("slave") public void create() { Slave slave = new Slave(); slave.setName("slave"); slave.setPassword("123465"); this.insert(slave); } }

主库测试

  [java]
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
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(); } }
  • 效果
  [plaintext]
1
2
3
4
5
6
7
mysql> select * from master; +----+--------+----------+ | id | name | password | +----+--------+----------+ | 1 | master | 123465 | +----+--------+----------+ 1 row in set (0.00 sec)

从库测试

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= Application.class) public class SlaveServiceTest { @Autowired private SlaveService slaveService; @Test public void insertTest() { slaveService.create(); } }
  • 效果
  [plaintext]
1
2
3
4
5
6
7
mysql> select * from slave; +----+-------+----------+ | id | name | password | +----+-------+----------+ | 1 | slave | 123465 | +----+-------+----------+ 1 row in set (0.00 sec)

小结

到这里,基于注解的动态数据源切换就实现了。

原理还是比较简单的,感觉这里可以封装为一个公共的方法,而不是每一次都花费时间去自己实现。

参考资料

springboot mybatis plus多数据源轻松搞定(下)