一、背景
mybatis自身没有做多数据源(包括读写分离)的支持,网上很多现有博文都是C&V产物,实现较老且缺乏对mybatis-spring的良好支持,故简单写过一个springboot的demo,仅依赖配置即可完成对多数据源的支持。
二、使用
1、项目中增加对应的maven依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- 非必需,方便使用 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
2、将以下两个个文件复制到项目中:
- 自定义配置映射类
import lombok.Data;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.springframework.beans.factory.support.BeanNameGenerator;
import java.io.Serializable;
/**
* mybatis复合数据源自动配置属性
* @author zxf
* @date 2020-08-16 20:26:13
*/
@Data
class MultiMybatisProperties implements Serializable {
/**
* dataSource连接池类型
*/
private Class<? extends javax.sql.DataSource> pool;
/**
* 连接池驱动类型
*/
private Class<? extends java.sql.Driver> driver;
/**
* 连接完整url
*/
private String url;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 扫描的包路径
*/
private String mapperLocations;
/**
* 扫描的mapper接口包集合
* @see MapperScan#basePackages()
*/
private String[] basePackages;
/**
* 组件命名器
* @see MapperScan#nameGenerator()
*/
private Class<BeanNameGenerator> namingClass;
/**
* dao映射代理类工厂类型
* @see MapperScan#factoryBean()
*/
private Class<? extends MapperFactoryBean<?>> factoryBean;
/**
* 是否懒加载
* @see MapperScan#lazyInitialization()
*/
private boolean lazyInitialization;
}
- 实际逻辑实现类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.util.Assert;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* mybatis多数据源扫描注册器
* @author zxf
* @date 2020-08-16 20:09:23
*/
@Slf4j
class MultiMybatisScannerRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private Environment env;
/** 作为模板的默认配置key **/
private static final String DEFAULT_KEY = "default";
private static final Pattern ID_MATCH = Pattern.compile("^[\\w]+$");
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
log.info("Ready to load properties of multi mybatis configs from environment.");
Map<String, MultiMybatisProperties> sources = processConfigs();
registerComponents(sources, registry);
log.info("Finish to count loading properties of multi mybatis configs from environment:" + sources.size());
}
/**
* 向spring容器中注册对应的相关mybatis组件
* 其中MapperScannerConfigurer 参考MapperScannerRegistrar的registerBeanDefinitions的实现
* @see org.mybatis.spring.annotation.MapperScannerRegistrar
* @param sources 有效的配置集合
* @param registry bean注冊器
*/
private void registerComponents(Map<String, MultiMybatisProperties> sources, BeanDefinitionRegistry registry) {
sources.forEach((key, value) -> {
Class<? extends BeanNameGenerator> clazz =
null == value.getNamingClass() ? DefaultBeanNameGenerator.class : value.getNamingClass();
BeanNameGenerator generator = BeanUtils.instantiateClass(clazz);
//DataSource
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(value.getPool());
//builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("jdbcUrl", value.getUrl());
builder.addPropertyValue("username", value.getUsername());
builder.addPropertyValue("password", value.getPassword());
builder.addPropertyValue("driverClassName", value.getDriver().getCanonicalName());
AbstractBeanDefinition definition = builder.getBeanDefinition();
String dataSourceName = generator.generateBeanName(definition, registry);
registry.registerBeanDefinition(dataSourceName, builder.getBeanDefinition());
//sqlSessionFactory
builder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionFactoryBean.class);
builder.addPropertyReference("dataSource", dataSourceName);
try {
builder.addPropertyValue("mapperLocations", new PathMatchingResourcePatternResolver().
getResources(value.getMapperLocations()));
} catch (IOException e) {
e.printStackTrace();
}
String SqlSessionFactoryName = generator.generateBeanName(definition, registry);
registry.registerBeanDefinition(SqlSessionFactoryName, builder.getBeanDefinition());
//DataSourceTransactionManager
builder = BeanDefinitionBuilder.genericBeanDefinition(DataSourceTransactionManager.class);
builder.addPropertyReference("dataSource", dataSourceName);
String transactionManagerName = generator.generateBeanName(definition, registry);
registry.registerBeanDefinition(transactionManagerName, builder.getBeanDefinition());
//SqlSessionTemplate
builder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionTemplate.class);
//builder.addPropertyValue("sqlSessionFactory", SqlSessionFactoryName);
builder.addConstructorArgReference(SqlSessionFactoryName);
String sqlSessionTemplateName = generator.generateBeanName(definition, registry);
registry.registerBeanDefinition(sqlSessionTemplateName, builder.getBeanDefinition());
//MapperScannerConfigurer
builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
if (!BeanNameGenerator.class.equals(clazz)) {
builder.addPropertyValue("nameGenerator", generator);
}
if (null != value.getFactoryBean()) {
builder.addPropertyValue("mapperFactoryBeanClass", value.getFactoryBean().getCanonicalName());
}
builder.addPropertyValue("sqlSessionTemplateBeanName", sqlSessionTemplateName);
List<String> basePackages = Arrays.stream(value.getBasePackages()).
filter(org.springframework.util.StringUtils::hasText).collect(Collectors.toList());
builder.addPropertyValue("basePackage",
org.springframework.util.StringUtils.collectionToCommaDelimitedString(basePackages));
builder.addPropertyValue("lazyInitialization", String.valueOf(value.isLazyInitialization()));
registry
.registerBeanDefinition(generator.generateBeanName(definition, registry), builder.getBeanDefinition());
});
}
/**
* 获取有效的配置集合
* @return java.util.List<com.resintec.lilliput.system.config.data.MultiMybatisProperties>
*/
private Map<String, MultiMybatisProperties> processConfigs(){
//首先确保在配置文件中能解析到对应的配置
Bindable<Map<String, MultiMybatisProperties>>bindable = Bindable.mapOf(String.class, MultiMybatisProperties.class);
BindResult<Map<String, MultiMybatisProperties>>bind = Binder.get(env).bind("mybatis.multi", bindable);
Assert.isTrue(null != bind && bind.isBound(),
"Failed to load multi mybatis dataSource due to no config found in th environment.");
//“default”关键字作为id将标识当前配置为模板属性配置,不作为独立的真实配置来处理
Map<String, MultiMybatisProperties> sources, finalSources = sources = bind.get();
sources.entrySet().stream().filter(en -> DEFAULT_KEY.equals(en.getKey())).findFirst().ifPresent(temp -> finalSources.values().forEach(s -> {
s.setUrl(StringUtils.isBlank(s.getUrl()) ? temp.getValue().getUrl() : s.getUrl());
s.setUsername(StringUtils.isBlank(s.getUsername()) ? temp.getValue().getUsername() : s.getUsername());
s.setPassword(StringUtils.isBlank(s.getPassword()) ? temp.getValue().getPassword() : s.getPassword());
s.setBasePackages(null == s.getBasePackages() ? temp.getValue().getBasePackages() : s.getBasePackages());
s.setMapperLocations(StringUtils.isBlank(s.getMapperLocations()) ? temp.getValue().getMapperLocations() : s.getMapperLocations());
s.setPool(null == s.getPool() ? temp.getValue().getPool() : s.getPool());
s.setDriver(null == s.getDriver() ? temp.getValue().getDriver() : s.getDriver());
s.setNamingClass(null == s.getNamingClass() ? temp.getValue().getNamingClass() : s.getNamingClass());
s.setFactoryBean(null == s.getFactoryBean() ? temp.getValue().getFactoryBean() : s.getFactoryBean());
}));
sources = finalSources.entrySet().stream().filter(en -> !DEFAULT_KEY.equals(en.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
//基本校验
sources.forEach((key, value) -> Assert.isTrue(
ID_MATCH.matcher(key).matches()
&& StringUtils.isNotBlank(value.getUrl())
&& StringUtils.isNotBlank(value.getUsername())
&& StringUtils.isNotBlank(value.getPassword()) && null != value.getBasePackages()
&& value.getBasePackages().length != 0
&& StringUtils.isNotBlank(value.getMapperLocations())
&& null != value.getPool()
&& null != value.getDriver()
//&& null != s.getNamingClass()
//&& null != s.getFactoryBean()
, "Failed to load multi mybatis dataSource due to the illegal config:" + value));
Assert.isTrue(!sources.isEmpty(),
"Failed to load multi mybatis dataSource due to no available config found in th environment.");
log.info("Find multi mybatis dataSource config from environment:{}.", sources.keySet());
return sources;
}
@Override
public void setEnvironment(Environment environment) {
env = environment;
}
}
3、在项目的 application.yml或者其他可纳入spring管理的配置中增加以下配置内容(以yml文件为例):
mybatis:
multi:
db1: #某个数据源的唯一标识;不要重复;“default”将会作为关键字标识此数据源为模板配置(不作为独立的数据源,仅供其他数据源参考的默认设置)
pool: com.zaxxer.hikari.HikariDataSource #使用的连接池类型
driver: com.mysql.jdbc.Driver #使用的数据库连接驱动
username: root #使用的数据库用户名
password: root #使用的数据库密码
url: jdbc:mysql://127.0.0.1:3306/test1 #使用的数据库url
lazy-initialization: true #是否懒加载
db2:
pool: com.zaxxer.hikari.HikariDataSource
driver: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/test2
lazy-initialization: true
db3:
pool: com.zaxxer.hikari.HikariDataSource
driver: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/test3
lazy-initialization: true
db4:
pool: com.zaxxer.hikari.HikariDataSource
driver: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/test4
lazy-initialization: true
三、扩展
1、该demo只是基本示范,在配置映射的类MultiMybatisProperties中只有几个最基本的属性,但是实际项目中dataSource、数据库连接池、mybatis-spring可能会需要更多的配置属性。只要在MultiMybatisProperties中增加对应属性并在MultiMybatisScannerRegistrar类的registerComponents中增加对该属性的解析即可。
2、关于读写分离,建议将读与写相关的mapper与dao放在不同的包下当作两个不同的数据源来处理即可。如果实在不想分包,建议从自己继承org.mybatis.spring.mapper.MapperFactoryBean入手,手动管理org.apache.ibatis.binding.MapperRegistry,在业务代码中通过aop或者ThreadLocal等手段灵活切换需要使用的org.apache.ibatis.binding.MapperMethod,需要处理的内容就比较多了就懒得写。
3、此demo中的属性映射对象MultiMybatisProperties由实现类从environment中读取,先于spring容器初始化完成,故无法使用@ConfigurationProperties来管理,这导致了该对象在yml等配置文件中缺乏对应的属性提示。用户可以将实现类MultiMybatisScannerRegistrar中的属性绑定关系由现在的map改为list,然后在/resources/META-INF/additional-spring-configuration-metadata.json(该文件对map支持不友好)中增加提示即可。
4、本示例支持本地事务回滚,多数据源的事务请自己通过集成seata等现有的成熟解决方案处理,此demo不涉及。