公司在做多租户的方案时,决定根据不同租户创建多个数据库。所以,请求调mybatis时,需要根据请求的某个参数去走不同的库,需要实现动态数据源切换。实现的时候遇到不少坑,今天看了一下mybatis源码,在这记录一下。
关键类:AbstractRoutingDataSource
这个类是spring-jdbc专门用来实现动态数据源切换的类。在项目启动的时候,我们可以把所有数据源根据key-value的形式放到这个类的targetDataSources里去。在调用数据库时,可以用determineCurrentLookupKey来决定当前调用(线程)使用哪个数据源。是的,这里可以用ThreadLocal来实现key的管理。
下面详细介绍:
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-jpa
com.alibaba
druid
1.0.31
org.springframework
spring-jdbc
5.1.5.RELEASE
org.mybatis
mybatis-spring
1.3.1
org.mybatis
mybatis
3.4.4
2..yml文件中定义数据源,我的方案是多数据源的name之间逗号隔开
mutiljdbc:
driverClassName: com.mysql.jdbc.Driver
url:jdbc:mysql://xxx.xx.xxx:3306/aaaa,jdbc:mysql://xxx.xx.xxx:3306/bbbb
username: aaa
password: aaa
这个依据个人想实现的方案而定。
3.动态解析上面的参数,动态生成DruidDataSource类型的springbean扔到spring容器里。这一步是为了给AbstractRoutingDataSource提供数据源做准备。
用一个配置类实现InitializingBean,让这个类在初始化的时候做这个事情;
简单说一下InitializingBean这个接口,它是spring提供的,可以在bean初始化的时候做一些自定义的事情,注意这个阶段spring容器未初始化完毕。代码如下:
/**
* 本bean初始化时,根据数据源个数,动态生成数据源bean
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
String urls = environment.getProperty("mutiljdbc.url");
if (urls != null) {
String[] urlArray = urls.split(",");
for (String url : urlArray) {
urlList.add(url);
}
for (String url : urlList) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
beanDefinitionBuilder.addPropertyValue("driverClassName", environment.getProperty("mutiljdbc.driverClassName"));
beanDefinitionBuilder.addPropertyValue("username", environment.getProperty("mutiljdbc.username"));
beanDefinitionBuilder.addPropertyValue("password", environment.getProperty("mutiljdbc.password"));
beanDefinitionBuilder.addPropertyValue("url", url);
BeanDefinition dataBeanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
//不知道怎么给bean取名字,直接用url作为bean的名字,去掉冒号和斜扛
url = url.replace("/", "").replace(":", "");
beanFactory.registerBeanDefinition(url, dataBeanDefinition);
}
}
}
其中urlList是个静态的全局list,专门放url。
4.自己实现一个类,继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法。
/**
* @author honc.z
* @date 2019/4/3
*
* 动态数据源(需要继承AbstractRoutingDataSource)
* 读写分离核心
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}
其中DatabaseContextHolder就是一个控制key的ThreadLocal.
/**
* @author honc.z
* @date 2019/4/3
*
* 线程安全的datebase容器
* threadlocal实现
*/
public class DatabaseContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static void setDatabaseType(String type) {
contextHolder.set(type);
}
public static String getDatabaseType() {
return contextHolder.get();
}
public static void remove(){
contextHolder.remove();
}
}
5.将DynamicDataSource作为springbean配置一下,把数据源放进去。
/**
* @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
*/
@Bean
@Primary
public DynamicDataSource dataSource() {
Map
这样,DynamicDataSource就会有一个默认的datasource(如果在请求过程中不使用determineCurrentLookupKey方法,就会走这个默认数据源),以及两个在yml中配置的数据源。
6.最后,把这个DynamicDataSource给Mybatis启动核心SqlSessionFactory。
/**
* 根据数据源创建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
// 指定数据源(这个必须有,否则报错)
fb.setDataSource(this.dataSource());
// 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
// fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));
// 指定基包
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(environment.getProperty("mybatis.mapper-locations")));
//把可能定义的拦截器扔进去
try {
String[] interceptors = applicationContext.getBeanNamesForType(Interceptor.class);
if (interceptors != null){
Interceptor[] interceptorsArray = new Interceptor[interceptors.length];
for (int i = 0;i
这里特别注意:如果有自定义的拦截器,必须要在这里手动加进去。因为没有引入mybatis-spring-boot-starter这个依赖(不能引,因为这个依赖会自动去找spring.datasource参数,而我们必须手动配置,找不到它会报错),这个依赖在定义启动类时,会去扫描所有的Interceptor(源码我就不贴了,在MybatisAutoConfiguration这个类里)。
到这里基本就结束了,在调用的时候,更具某个参数去调用DatabaseContextHolder.setDatabaseType(key);就可以实现指定数据源。这个地方的key,我是根据来数据源url去匹配请求中的某个参数来设定,这个方法不太好,限制比较大。我的解决方案是提供一个接口让开发去设置key,然后让开发根据参数来匹配。
另外,还有一个必须要注意的地方,springboot启动类注解必须加上@SpringBootApplication(exclude={DataSourceAutoConfiguration.class}),这个究竟是为什么暂时没时间看。
下面是源码地址,https://github.com/skesunny/sample-all,在sample-datasource这个module下面。