springboot + mybatis + 多数据源
在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一个数据源。
代码结构:

简要原理:
1)DatabaseType列出所有的数据源的key---key
2)DatabaseContextHolder是一个线程安全的DatabaseType容器,并提供了向其中设置和获取DatabaseType的方法
3)DynamicDataSource继承AbstractRoutingDataSource并重写其中的方法determineCurrentLookupKey(),在该方法中使用DatabaseContextHolder获取当前线程的DatabaseType
4)MyBatisConfig中生成2个数据源DataSource的bean---value
5)MyBatisConfig中将1)和4)组成的key-value对写入到DynamicDataSource动态数据源的targetDataSources属性(当然,同时也会设置2个数据源其中的一个为DynamicDataSource的defaultTargetDataSource属性中)
6)将DynamicDataSource作为primary数据源注入到SqlSessionFactory的dataSource属性中去,并且该dataSource作为transactionManager的入参来构造DataSourceTransactionManager
7)使用的时候,在dao层或service层先使用DatabaseContextHolder设置将要使用的数据源key,然后再调用mapper层进行相应的操作,建议放在dao层去做(当然也可以使用spring aop+自定注解去做)
注意:在mapper层进行操作的时候,会先调用determineCurrentLookupKey()方法获取一个数据源(获取数据源:先根据设置去targetDataSources中去找,若没有,则选择defaultTargetDataSource),之后在进行数据库操作。
1、假设有两个数据库,配置如下
application.properties
1 #the first datasource
2 jdbc.driverClassName = com.mysql.jdbc.Driver
3 jdbc.url = jdbc:mysql://xxx:3306/mytestdb?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
4 jdbc.username = root
5 jdbc.password = 123
6
7 #the second datasource
8 jdbc2.driverClassName = com.mysql.jdbc.Driver
9 jdbc2.url = jdbc:mysql://xxx:3306/mytestdb2?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
10 jdbc2.username = root
11 jdbc2.password = 123
说明:在之前的配置的基础上,只增加了上述的第二个数据源。
2、DatabaseType
1 package com.xxx.firstboot.common.datasource;
2
3 /**
4 * 列出所有的数据源key(常用数据库名称来命名)
5 * 注意:
6 * 1)这里数据源与数据库是一对一的
7 * 2)DatabaseType中的变量名称就是数据库的名称
8 */
9 public enum DatabaseType {
10 mytestdb,mytestdb2
11 }
作用:列举数据源的key。
3、DatabaseContextHolder
1 package com.xxx.firstboot.common.datasource;
2
3 /**
4 * 作用:
5 * 1、保存一个线程安全的DatabaseType容器
6 */
7 public class DatabaseContextHolder {
8 private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
9
10 public static void setDatabaseType(DatabaseType type){
11 contextHolder.set(type);
12 }
13
14 public static DatabaseType getDatabaseType(){
15 return contextHolder.get();
16 }
17 }
作用:构建一个DatabaseType容器,并提供了向其中设置和获取DatabaseType的方法
4、DynamicDataSource
1 package com.xxx.firstboot.common.datasource;
2
3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
4
5 /**
6 * 动态数据源(需要继承AbstractRoutingDataSource)
7 */
8 public class DynamicDataSource extends AbstractRoutingDataSource {
9 protected Object determineCurrentLookupKey() {
10 return DatabaseContextHolder.getDatabaseType();
11 }
12 }
作用:使用DatabaseContextHolder获取当前线程的DatabaseType
5、MyBatisConfig
1 package com.xxx.firstboot.common;
2
3 import java.util.HashMap;
4 import java.util.Map;
5 import java.util.Properties;
6
7 import javax.sql.DataSource;
8
9 import org.apache.ibatis.session.SqlSessionFactory;
10 import org.mybatis.spring.SqlSessionFactoryBean;
11 import org.mybatis.spring.annotation.MapperScan;
12 import org.springframework.beans.factory.annotation.Autowired;
13 import org.springframework.beans.factory.annotation.Qualifier;
14 import org.springframework.context.annotation.Bean;
15 import org.springframework.context.annotation.Configuration;
16 import org.springframework.context.annotation.Primary;
17 import org.springframework.core.env.Environment;
18 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
19 import org.springframework.jdbc.datasource.DataSourceTransactionManager;
20
21 import com.alibaba.druid.pool.DruidDataSourceFactory;
22 import com.xxx.firstboot.common.datasource.DatabaseType;
23 import com.xxx.firstboot.common.datasource.DynamicDataSource;
24
25 /**
26 * springboot集成mybatis的基本入口 1)创建数据源(如果采用的是默认的tomcat-jdbc数据源,则不需要)
27 * 2)创建SqlSessionFactory 3)配置事务管理器,除非需要使用事务,否则不用配置
28 */
29 @Configuration // 该注解类似于spring配置文件
30 @MapperScan(basePackages = "com.xxx.firstboot.mapper")
31 public class MyBatisConfig {
32
33 @Autowired
34 private Environment env;
35
36 /**
37 * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
38 */
39 @Bean
40 public DataSource myTestDbDataSource() throws Exception {
41 Properties props = new Properties();
42 props.put("driverClassName", env.getProperty("jdbc.driverClassName"));
43 props.put("url", env.getProperty("jdbc.url"));
44 props.put("username", env.getProperty("jdbc.username"));
45 props.put("password", env.getProperty("jdbc.password"));
46 return DruidDataSourceFactory.createDataSource(props);
47 }
48
49 @Bean
50 public DataSource myTestDb2DataSource() throws Exception {
51 Properties props = new Properties();
52 props.put("driverClassName", env.getProperty("jdbc2.driverClassName"));
53 props.put("url", env.getProperty("jdbc2.url"));
54 props.put("username", env.getProperty("jdbc2.username"));
55 props.put("password", env.getProperty("jdbc2.password"));
56 return DruidDataSourceFactory.createDataSource(props);
57 }
58
59 /**
60 * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
61 * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
62 */
63 @Bean
64 @Primary
65 public DynamicDataSource dataSource(@Qualifier("myTestDbDataSource") DataSource myTestDbDataSource,
66 @Qualifier("myTestDb2DataSource") DataSource myTestDb2DataSource) {
67 Map<Object, Object> targetDataSources = new HashMap<>();
68 targetDataSources.put(DatabaseType.mytestdb, myTestDbDataSource);
69 targetDataSources.put(DatabaseType.mytestdb2, myTestDb2DataSource);
70
71 DynamicDataSource dataSource = new DynamicDataSource();
72 dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
73 dataSource.setDefaultTargetDataSource(myTestDbDataSource);// 默认的datasource设置为myTestDbDataSource
74
75 return dataSource;
76 }
77
78 /**
79 * 根据数据源创建SqlSessionFactory
80 */
81 @Bean
82 public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception {
83 SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
84 fb.setDataSource(ds);// 指定数据源(这个必须有,否则报错)
85 // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
86 fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
87 fb.setMapperLocations(
88 new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));//
89
90 return fb.getObject();
91 }
92
93 /**
94 * 配置事务管理器
95 */
96 @Bean
97 public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
98 return new DataSourceTransactionManager(dataSource);
99 }
100
101 }
作用:
- 通过读取application.properties文件生成两个数据源(myTestDbDataSource、myTestDb2DataSource)
- 使用以上生成的两个数据源构造动态数据源dataSource通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)
- @Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
- @Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
- @Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
6、使用
ShopMapper:
1 package com.xxx.firstboot.mapper;
2
3 import org.apache.ibatis.annotations.Param;
4 import org.apache.ibatis.annotations.Result;
5 import org.apache.ibatis.annotations.Results;
6 import org.apache.ibatis.annotations.Select;
7
8 import com.xxx.firstboot.domain.Shop;
9
10 public interface ShopMapper {
11
12 @Select("SELECT * FROM t_shop WHERE id = #{id}")
13 @Results(value = { @Result(id = true, column = "id", property = "id"),
14 @Result(column = "shop_name", property = "shopName") })
15 public Shop getShop(@Param("id") int id);
16
17 }
ShopDao:
1 package com.xxx.firstboot.dao;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.stereotype.Repository;
5
6 import com.xxx.firstboot.common.datasource.DatabaseContextHolder;
7 import com.xxx.firstboot.common.datasource.DatabaseType;
8 import com.xxx.firstboot.domain.Shop;
9 import com.xxx.firstboot.mapper.ShopMapper;
10
11 @Repository
12 public class ShopDao {
13 @Autowired
14 private ShopMapper mapper;
15
16 /**
17 * 获取shop
18 */
19 public Shop getShop(int id) {
20 DatabaseContextHolder.setDatabaseType(DatabaseType.mytestdb2);
21 return mapper.getShop(id);
22 }
23 }
注意:首先设置了数据源的key,然后调用mapper(在mapper中会首先根据该key从动态数据源中查询出相应的数据源,之后取出连接进行数据库操作)
ShopService:
1 package com.xxx.firstboot.service;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.stereotype.Service;
5
6 import com.xxx.firstboot.dao.ShopDao;
7 import com.xxx.firstboot.domain.Shop;
8
9 @Service
10 public class ShopService {
11
12 @Autowired
13 private ShopDao dao;
14
15 public Shop getShop(int id) {
16 return dao.getShop(id);
17 }
18 }
ShopController:
1 package com.xxx.firstboot.web;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.web.bind.annotation.RequestMapping;
5 import org.springframework.web.bind.annotation.RequestMethod;
6 import org.springframework.web.bind.annotation.RequestParam;
7 import org.springframework.web.bind.annotation.RestController;
8
9 import com.xxx.firstboot.domain.Shop;
10 import com.xxx.firstboot.service.ShopService;
11
12 import io.swagger.annotations.Api;
13 import io.swagger.annotations.ApiOperation;
14
15 @RestController
16 @RequestMapping("/shop")
17 @Api("shopController相关api")
18 public class ShopController {
19
20 @Autowired
21 private ShopService service;
22
23 @ApiOperation("获取shop信息,测试多数据源")
24 @RequestMapping(value = "/getShop", method = RequestMethod.GET)
25 public Shop getShop(@RequestParam("id") int id) {
26 return service.getShop(id);
27 }
28
29 }
补:其实DatabaseContextHolder和DynamicDataSource完全可以合为一个类
参考:
http://www.cnblogs.com/lzrabbit/p/3750803.html
遗留:在实际开发中,一个dao类只会用到一个数据源,如果dao类中的方法很多的话,每一个方法前边都要添加一个设置数据源的一句话,代码有些冗余,可以使用AOP切面。
具体的实现方式见 第九章 springboot + mybatis + 多数据源 (AOP实现)
在第八章 springboot + mybatis + 多数据源代码的基础上,做两点修改
1、ShopDao
1 package com.xxx.firstboot.dao;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.stereotype.Repository;
5
6 import com.xxx.firstboot.domain.Shop;
7 import com.xxx.firstboot.mapper.ShopMapper;
8
9 @Repository
10 public class ShopDao {
11 @Autowired
12 private ShopMapper mapper;
13
14 /**
15 * 获取shop
16 */
17 public Shop getShop(int id) {
18 return mapper.getShop(id);
19 }
20 }
说明:只是去掉了设置数据源key的那一句代码
2、DataSourceAspect
View Code说明:列出了两种切面方法,在这里推荐使用前者,原因:
- @Around:需要写执行目标方法的那一行代码,而这一行代码可能会抛异常,还需要抛出或捕获
对于切点表达式,可以抽取出来,进行重复利用。如上代码可以改为如下:
1 package com.xxx.firstboot.common.datasource;
2
3 import org.aspectj.lang.JoinPoint;
4 import org.aspectj.lang.annotation.Aspect;
5 import org.aspectj.lang.annotation.Before;
6 import org.aspectj.lang.annotation.Pointcut;
7 import org.springframework.stereotype.Component;
8
9 import com.xxx.firstboot.dao.ShopDao;
10
11 @Aspect
12 @Component
13 public class DataSourceAspect {
14
15 /**
16 * 使用空方法定义切点表达式
17 */
18 @Pointcut("execution(* com.xxx.firstboot.dao.*.*(..))")
19 public void declareJointPointExpression() {
20 }
21
22 /**
23 * 使用定义切点表达式的方法进行切点表达式的引入
24 */
25 @Before("declareJointPointExpression()")
26 public void setDataSourceKey(JoinPoint point) {
27 // 连接点所属的类实例是ShopDao
28 if (point.getTarget() instanceof ShopDao) {
29 DatabaseContextHolder.setDatabaseType(DatabaseType.mytestdb2);
30 } else {// 连接点所属的类实例是UserDao(当然,这一步也可以不写,因为defaultTargertDataSource就是该类所用的mytestdb)
31 DatabaseContextHolder.setDatabaseType(DatabaseType.mytestdb);
32 }
33 }
34
35 }
注意:该切点表达式也可以用在其他切面类中,引入的时候使用"全类名.切点方法名()",例:@Before("com.xxx.firstboot.common.datasource.DataSourceAspect.declareJointPointExpression()")

浙公网安备 33010602011771号